Linking two separate cmake projects where one is dependant on the other - c++

I have two separate projects, A and B. Both have large folder structures, lots of dependencies and both have their own CMakelists.txt structures as well. Neither depend on each other.
I am adding new functionality to project A that depends on a header from project B, along with the network of files in Project B that header depends on.
I am at a loss for the correct way to link these with cmake. I added to header as an include where necessary in my code, but am not sure the correct way to link it. Do I use target_include_directories and add the binaries from project B, or the source code, or both? Project A won't be building B as it is already set up, with the binaries and source in separate subdirectories. I just need A to be able to call the functionality from B.
I am not very familiar with cmake yet, so any pointers would be appreciated.
Thanks.

Project B should export its targets.
That way, in project A, you could do something like this:
find_package(B REQUIRED)
target_link_libraries(A PUBLIC B::B)
And when invoking CMake, add B's build directory in the prefix path:
# in A/build
cmake .. -DCMAKE_PREFIX_PATH=/path/to/B/build
But in order for that to work, you must change your B CMake files to export your targets (as it should):
include(GNUInstallDirs)
add_library(B INTERFACE)
add_library(B::B ALIAS B)
target_link_libraries(B PUBLIC dependency-of-B)
target_compile_features(B INTERFACE cxx_std_17) # or any language version
# Set B include directory. Will add include directory for both B and A
target_include_directories(B PUBLIC
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/your-include-dir>
)
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/cmake/B-config.cmake" DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
install(TARGETS B EXPORT B-targets)
# install package
install(EXPORT B-targets
FILE B-targets.cmake
NAMESPACE B::
DESTINATION share/cmake/B
)
# export build tree
export(
EXPORT B-targets
FILE "${CMAKE_CURRENT_BINARY_DIR}/B-targets.cmake"
NAMESPACE B::
)
With B-config.cmake:
include(CMakeFindDependencyMacro)
# All find_package we used in CMakeLists.txt should also be there
find_dependency(dependency1 REQUIRED)
find_dependency(dependency2 REQUIRED)
include("${CMAKE_CURRENT_LIST_DIR}/B-targets.cmake")
If you have many target in B, you should install(TARGETS other-target EXPORT B-targets)
I strongly recommend looking at example of libraries that export their targets so you can base your work on it. For example, the glm library export its targets and always worked well. I also added target exportation in msdfgen project.

If you only want to use headers and not object files then just target_include_directories is all you need. Dependency tracking will be done by cmake and ninja/make automatically.
Alternatively, if you need to include both headers and objects from B, you'd really want to factor out these bits into small stand-alone static libraries within B, and then link with those libraries from A - this will add include file paths and everything else needed. cmake is quite good at it.

Related

Exporting and packaging prebuilt libraries in cmake

instead of asking a question directly, I'll expose my use case and the way I tried (but failed) to solve it.
Say I have:
3 shared libraries A, B and C
A require B and C
A comes with a set of headers
That's it, no extra information, it's provided by a vendor and not possibly subject to any change (modernization/cmake packages, etc).
A should always be packaged with B and C. I should only need to link with A and cmake should transitively link with B and C.
Now, I'd like to make it more "modern cmake" friendly and by able to:
First usecase: Create a repo containing these libs and calling add_subdirectory() from a parent project.
First usecase: Create a package (say debian pkg . deb) containing the relevant AConfig.cmake AConfigVersion.cmake and ATargets.cmake. Then a simple system install of the pkg and a find_package() should to the trick.
What has been done:
I tried using INTERFACE IMPORTED library and INTERFACE.
Because I want to support packaging the libs INTERFACE IMPORTED can't be used (you can't install it as far as I know/tested).
INTERFACE is working fine for the first usecase, using add_subdirectory(), headers are found, everything links, but because the user may not have at this point, the shared lib in is path, he can't run the tests for instance.
Then comes the export part needed to make the shared libs available and to make find_package() work. I succeed to export/package the libs A B C, the headers for A and the files needed for find_package().
But when in a client library D, find_package(A REQUIRED) finds the lib (no messages such as "Could not find a package configuration file provided by "A" ") it doess NOT create any target I can link on. I took a look at the generated ATargets.cmake:
# generated stuff before
add_library(A::A INTERFACE IMPORTED) # This is cannot be used at all, does not create a target I can link on unless I remove IMPORTED
set_target_properties(A::A PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include" # that's fine
INTERFACE_LINK_LIBRARIES "A.so;B.so.1.0;C.so.1.0" # that's not fine I need ${_IMPORT_PREFIX}/${CMAKE_INSTALL_LIBDIR}/ before each libs like it did for the headers
)
# generated stuff after
Note that if I remove the IMPORTED in the add_library of the ATargets.cmake and remove the namespace stuff, the target A is correctly accessible in any client cmake project using find_package but it'll NOT link correctly probably because A.so (and B and C) is not referenced using ${_IMPORT_PREFIX}/lib/A.so. I tried adding ${_IMPORT_PREFIX}/lib/ before all libs in INTERFACE_LINK_LIBRARIES, removed the IMPORTED keyword and namespace stuff and guess what, it works perfectly... Now, how do I do that without the trick.
The content of AConfig.cmake.in is:
#PACKAGE_INIT#
include(CMakeFindDependencyMacro)
# Add the targets file
include("${CMAKE_CURRENT_LIST_DIR}/ATargets.cmake")
# check_required_components(#PROJECT_NAME#) # It is commented because unless I apply the fix specified earlier (IMPORTED and namespace removed), during the find_package call I get:
#################
# CMake Error at /usr/lib/cmake/A/AConfig.cmake:8 # (check_required_components):
# Unknown CMake command "check_required_components".
#################
That's pretty much it, the rest of this post is implementation details. This code below can be used to solve use case 1 but the export package and cmake config/target files are not correct.
add_library(A
INTERFACE)
add_library(A::A ALIAS A)
target_include_directories(A
INTERFACE "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
"$<INSTALL_INTERFACE:include>")
target_link_libraries(A
INTERFACE "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/lib/A.so>"
"$<INSTALL_INTERFACE:A.so>"
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/lib/B.so>"
"$<INSTALL_INTERFACE:B.so>"
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/lib/C.so>"
"$<INSTALL_INTERFACE:C.so>")
#### install
install(TARGETS A
EXPORT ATargets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT A_Runtime
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT A_Runtime NAMELINK_COMPONENT A_Development
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT A_Development)
install(DIRECTORY "lib/"
DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include"
DESTINATION "include")
write_basic_package_version_file(AConfigVersion.cmake
VERSION "${PACKAGE_VERSION}"
COMPATIBILITY SameMajorVersion)
install(EXPORT ATargets
FILE ATargets.cmake
NAMESPACE A::
DESTINATION "lib/cmake/A")
configure_file(AConfig.cmake.in AConfig.cmake #ONLY)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/AConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/AConfigVersion.cmake"
DESTINATION "lib/cmake/A")
# Then some cpack stuff that is not affecting the work done earlier
In a client lib/cmake project after the installation of the generated package (generated by cpack):
find_package(A)
target_link_libraries(my_project PUBLIC/PRIVATE A::A) # Should bring in the headers and link with A B and C
Documentation:
cmake: create a new library target which consists of a prebuilt library
https://gitlab.kitware.com/cmake/community/-/wikis/doc/tutorials/Exporting-and-Importing-Targets
Possible to add an imported library to target_link_libraries that takes care of include directories too?
Exporting an imported library
https://discourse.cmake.org/t/how-to-control-import-prefix-in-exported-targets-cmake-list-file-generated-by-cmake/2291
Create relocatable package with proper autogenerated config cmake
https://discourse.cmake.org/t/exporting-packages-with-a-custom-find-module/3820/2
Thanks for reading/helping

Accessing an external project with add-subdirectory results in CMake Error related to export set

I have a project A that depends on spdlog. Here is the structure:
|--- dir A
...|---src
......|---CMakeLists.txt
...|---include
...|---CMakeLists.txt
|---external/3rd_party/spdlog
I am trying to access spdlog in project A by adding a subdirectory. Here is how my A/CMakeLists.txt looks like:
cmake_minimum_required(VERSION 3.9.3 FATAL_ERROR)
project(GLOBAL CXX)
add_subdirectory(../external/3rd_party/spdlog ${CMAKE_BINARY_DIR}/spdlog EXCLUDE_FROM_ALL)
add_subdirectory(src)
Here is how my A/src/CMakeLists.txt looks like:
cmake_minimum_required(VERSION 3.9.3 FATAL_ERROR)
project(Alib CXX)
if(NOT TARGET spdlog)
# Stand-alone build
find_package(spdlog_header_only REQUIRED)
endif()
add_librray(A A.cpp)
target_link_libraries(A PUBLIC spdlog_header_only)
install(TARGETS A
EXPORT ATargets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
install(EXPORT ATargets
NAMESPACE A::
FILE ATargets.cmake
DESTINATION ${INSTALL_CONFIGDIR})
install(FILES AConfig.cmake DESTINATION ${INSTALL_CONFIGDIR})
When I try to build this, I get the following error:
CMake Error: install(EXPORT "ATargets" ...) includes target "A" which requires target "spdlog_header_only" that is not in the export set.
Please can you suggest me how to fix it?
For some reason I need to maintain the same directory structure I have shown above.
Here is a related question but does not have an answer : here
Meaining of the error
Since you use PUBLIC keyword when link your library (A) with spdlog_header_only, CMake expects that this linking is also needed for users of your library. So, when you create config file for your library (with install(EXPORT)), CMake adds linking with spdlog_header_only target into the config file too. Like
# [ATargets.cmake]
target_link_libraries(A::A PUBLIC spdlog_header_only)
Linking with a target implies existence of this target, but you do not install spdlog_header_only target. Because of that, created config file for your library won't work. This is what CMake tells you in the error message.
Simple fix
The simplest fix would be using PRIVATE linking with spdlog_header_only target, so that linking won't be part of the config file. Beware, in that case a user of your (installed) library won't get access to the header files for spdlog.
(But a user could obtain these headers by other means.)
Hard fix
But if you want a user of your library to have access to spdlog headers (or, worse, the public headers of your library use #include for headers from spdlog), then you cannot drop PUBLIC linking. In that case you need to install spdlog_header_only target too. E.g. by enabling SPDLOG_INSTALL option
set(SPDLOG_INSTALL ON)
before
add_subdirectory(../external/3rd_party/spdlog ${CMAKE_BINARY_DIR}/spdlog EXCLUDE_FROM_ALL)
(Note, that aside from enabling SPDLOG_INSTALL option, several other adjustments needs to be done for make the config file for your library to work.)

CMake Package Support - Includes and Libraries not found

I am currently developing a software package, for which I'd like to provide the cmake package support (so users can find it with find_package(...)). The Problem is, the package is found but FOO_INCLUDE_DIR and FOO_LIBRARIES is empty.
Within my package I have several modules, each with a CMakeLists file which installs the respective library and headers with:
install(TARGETS ${LIBRARY_NAME} EXPORT FooTargets
RUNTIME DESTINATION ${Foo_RUNTIME_INSTALL_DIR}
LIBRARY DESTINATION ${Foo_LIBRARY_INSTALL_DIR}
ARCHIVE DESTINATION ${Foo_ARCHIVE_INSTALL_DIR}
FRAMEWORK DESTINATION ${Foo_FRAMEWORK_INSTALL_DIR})
# Headers
install(
DIRECTORY include/${LIBRARY_NAME}
DESTINATION include/${PROJECT_NAME}
FILES_MATCHING
PATTERN "*.h"
PATTERN "*.hpp"
)
The headers for the library are included with target_include_directories like this:
target_include_directories(${LIBRARY_NAME} PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> # for headers when building
$<INSTALL_INTERFACE:${Foo_INC_INSTALL_DIR}> # for client in install mode
)
I checked the folders and all libraries and headers are correctly installed. In my toplevel CMakeLists I export my targets with:
install(
EXPORT FooTargets
DESTINATION ${Foo_CMAKE_CONFIG_INSTALL_DIR}
FILE FooConfig.cmake
)
The config is where I assume it to be (usr/local/lib/cmake/Foo). So everything seems to be correct. When I look into my FooConfig.cmake it says:
# Create imported target realm_densifier_base
add_library(FooLib1 SHARED IMPORTED)
set_target_properties(FooLib1 PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "/usr/local/include/Foo"
INTERFACE_LINK_LIBRARIES "...several libraries..."
)
...which is absolutely correct and exactly what I expected. What part of the puzzle is missing? Is INTERFACE_INCLUDE_DIRECTORIES and INTERFACE_LINK_LIBRARIES not the correct flag to be set?
Thanks for the help and best regards,
Alex
Edit:
#Guillaume Racicot already cleared most things up, I only knew the "non target" way of adding headers to my project, that was with include_directories(Foo_INCLUDE_DIRS). However, in the target-world linking against my library Foo was enough. Another thing was that I messed up some directories in the target_include_directories(...) command, so directories were wrong and therefore could not be found in my other project. Thanks for the help!
Why would FOO_INCLUDE_DIR or FOO_LIBRARIES be set? This may be how old find modules worked, but not how config files work. Even newer find modules expose targets instead of directory variables.
When generating an XYZConfig.cmake file, informations about targets will be exported, not informations on the directory.
With such exportation:
install(
EXPORT FooTargets
NAMESPACE Foo::
DESTINATION ${Foo_CMAKE_CONFIG_INSTALL_DIR}
FILE FooConfig.cmake
)
You would expect users of the package to use it like so:
find_package(Foo REQUIRED)
# or PUBLIC ------v
target_link_libraries(bar PRIVATE Foo::FooLib1)
If your package has multiple targets in the export set, then you can link to both or only one
target_link_libraries(bar PRIVATE Foo::FooLib1 Foo::FooLib2)
target_link_libraries(baz PUBLIC Foo::FooLib2) # link to lib2 only
When you link to an exported target like Foo::FooLib1, its public interface will be transitively transmitted to its user. In the example above, bar will inherit properties from the linked target.
So the INTERFACE_INCLUDE_DIRECTORIES of Foo::FooLib1 and Foo::FooLib2 will be appended to bar's INCLUDE_DIRECTORIES. Same for LINK_LIBRARIES.
For baz, not only its INCLUDE_DIRECTORIES will contain Foo::FooLib2 entries, but also its own INTERFACE_INCLUDE_DIRECTORIES will transitively transmit the usage requirements of Foo::FooLib2

How to write cmake modules for "boost-like" multi-component library?

I'm currently writing a c++ library, that has several "sub libraries", like for example the boost library.
Let's name the library "TestLib" and the sub libraries "Base" and "Ext", where Base contains come basic stuff, that doesn't depend an any other sub library. Ext however depends on some classes of Base.
Each "sub library" should compile into a separate .a or .so file, but they all should share a namespace (TestLib).
Now i'm aiming to write clean cmake scripts in order to achieve this goal.
In the end i want to be able to do something like this in cmake:
find_package(TestLib 0.1 REQUIRED COMPONENTS Base Ext)
or
target_link_libraries(someapplication
PUBLIC
TestLib::Base
)
I have put each "sub library" in a separate git repository and added them as submodules in a new repository that has only a CMakeLists.txt
that just calls add_subdirectory on each of the repos.
Most of the cmake stuff i achieved, i got from this awesome tutorial at
https://pabloariasal.github.io/
And the Base part works as intended (which is no wonder, since it doesn't depend on anything else).
But my problems come with the Ext part. In order for this to compile, i have to link against the Base library, which shouldn't be to hard, and with some trial and error i am sure that i will get it to work.
But i want to do it the right way.
My approach was to
find_package(TestLib COMPONENTS Base)
in the CMakeLists.txt of TestLib.Ext.
But this cant be found since it has no TestLibConfig.cmake.
Which makes sense, but i don't know what to put in this file.
I tried to provide some code that exactly describes my problem, but since this would be too much to post here, i created a github for this purpose:
https://github.com/PowerSupplyTopologies/TestLib
This should contain all the relevant code.
This could be trivial to some of you, but i bet there are more people that could benefit from this approach.
Thank you in advance for any of your thoughts.
EDIT:
The library creation in the CMakeLists.txt of Base is:
set(TARGET_NAME testlibbase)
add_library(${TARGET_NAME}
src/ClassA.cpp
src/ClassB.cpp
)
#Add an alias so that library can be used inside the build tree, e.g. when testing
add_library(TestLib::${TARGET_NAME} ALIAS ${TARGET_NAME})
and
set(INSTALL_CONFIGDIR ${CMAKE_INSTALL_LIBDIR}/cmake/TestLib)
install(TARGETS ${TARGET_NAME}
EXPORT ${TARGET_NAME}-targets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
#This is required so that the exported target has the name JSONUtils and not jsonutils
set_target_properties(${TARGET_NAME} PROPERTIES EXPORT_NAME Base)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
#Export the targets to a script
install(EXPORT ${TARGET_NAME}-targets
FILE
TestLibBaseTargets.cmake
NAMESPACE
TestLib::
DESTINATION
${INSTALL_CONFIGDIR}
)
#Create a ConfigVersion.cmake file
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/TestLibBaseConfigVersion.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY AnyNewerVersion
)
configure_package_config_file(${CMAKE_CURRENT_LIST_DIR}/cmake /TestLibBaseConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/TestLibBaseConfig.cmake
INSTALL_DESTINATION ${INSTALL_CONFIGDIR}
)
#Install the config, configversion and custom find modules
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/TestLibBaseConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/TestLibBaseConfigVersion.cmake
DESTINATION ${INSTALL_CONFIGDIR}
)
##############################################
## Exporting from the build tree
export(EXPORT ${TARGET_NAME}-targets FILE ${CMAKE_CURRENT_BINARY_DIR}/TestLibBaseTargets.cmake NAMESPACE TestLib::)
and of Ext:
set(TARGET_NAME testlibext)
add_library(${TARGET_NAME}
src/ClassC.cpp
)
#Add an alias so that library can be used inside the build tree, e.g. when testing
add_library(TestLib::${TARGET_NAME} ALIAS ${TARGET_NAME})
In your meta project TestLib you can create a TestLibConfig.cmake file like described in the CMake documentation.
TestLibConfig.cmake:
set(_supported_components Base Ext)
foreach(_comp ${Test_FIND_COMPONENTS})
if (NOT ";${_supported_components};" MATCHES _comp)
set(TestLib_FOUND False)
set(TestLib_NOT_FOUND_MESSAGE "Unsupported component: ${_comp}")
endif()
include("${CMAKE_CURRENT_LIST_DIR}/TestLib${_comp}Targets.cmake")
endforeach()
ref: https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html#creating-a-package-configuration-file

cmake: how to reference and build separate cmake project dependency?

I have a cross-compiler cmake project that depends on libraries from a separate project that happens to also use cmake:
/myProject/CMakeLists.txt (uses cross-compiler)
/anotherProject/CMakeLists.txt (platform-agnostic)
anotherProject can be built completely separately on its own. It has no knowledge of myProject at all.
Now, anotherProject has numerous modules that I need, like:
anotherProject/A/CMakeLists.txt (produces static lib A.a)
anotherProject/B/CMakeLists.txt (produces static lib B.a)
etc
When I build myProject, I want to build and link against anotherProject/A and anotherProject/B, to produce shared lib myproject.so. I'd like to leverage the existing cmake-ness of anotherProject if possible, as opposed to manually globbing its various source sets from myProject.
What's the correct way to achieve this with cmake? I feel like I'm missing something obvious.
It would be straightforward if, say, myProject were just a subdirectory under anotherProject, or if there were a top-level CMakeLists.txt that could reference both myProject and anotherProject; but neither is what I'm after. I know I could build anotherProject and export its libraries to a well-known location, and then reference the export directory from myProject - but I would like to avoid that setup as well.
A solution is to use CMake packages.
Basically, in anotherProject, you craft a CMake configuration file where you set variables to be used by myProject (eg. include directory, list of libraries, compilation flags...), or even targets.
Then, in myProject, you use the find_package() mechanism so that CMake finds this configuration file and imports the variables/targets in your current project.
There is a tutorial on the CMake wiki.
The only alternative setup that I can think of based on your requirements is to allow your main (dependent) project to discover the other (dependee) project using find_package.
In your main project CMakeLists.txt you should add something like this:
find_package(anotherProject CONFIG)
if(anotherProject_FOUND)
message(STATUS "Found project dependency: anotherProject")
else
# change WARNING to FATAL_ERROR if the dependency is NOT optional
message(WARNING "package anotherProject was not found")
endif()
On the differences between CONFIG and MODULE modes, check the documentation and this link.
Then assuming that your main project creates an executable, you could hook up the discovered dependency like this:
add_executable(myProject ${SOURCES})
[...]
if(anotherProject_FOUND)
target_link_libraries(myProject PUBLIC anotherProject)
endif()
This should take care of the required include files and definitions as well.
Now in the dependee project CMakeLists.txt you should do something like this:
set(PRJ_NAME "anotherProject")
string(TOLOWER ${PRJ_NAME} PRJ_NAME_LOWER)
set(ANOTHERPROJECT_EXPORT_NAME "${PRJ_NAME}")
install(TARGETS ${PRJ_NAME} EXPORT ${ANOTHERPROJECT_EXPORT_NAME}
RUNTIME DESTINATION .)
install(EXPORT ${ANOTHERPROJECT_EXPORT_NAME} DESTINATION "share/cmake")
This associates an export with a target and then installs the export.
Now, if you check that export file, it expects certain things to be found and included, that could be specific for your project. To make this as supple as possible, you can use the configure feature to generate them from a template and then install from the build directory.
So, in the project under a subdir named share/cmake you could have a file named config.cmake.in with contents:
include(${CMAKE_CURRENT_LIST_DIR}/#PRJ_NAME#.cmake)
In the main project's CMakeLists.txt you need to add the following for generating the file from that template:
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/share/cmake/config.cmake
${CMAKE_CURRENT_BINARY_DIR}/share/cmake/${PRJ_NAME_LOWER}-config.cmake)
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/share/
DESTINATION share)
Notice that I used PRJ_NAME, because you could potentially reuse that to name the actual executable at the add_executable command. It mentally helps if the exported target has the same name with produced one.
This is a more versatile version to accommodate multiple subprojects of this tutorial.