Once again, the root CMakeLists.txt file is unchanged with respect to the previous recipe. Moving onto the leaf directory src containing our sources:
- We need to find the UUID library and we can re-use the code used in previous recipes:
# Search for pkg-config and UUID
find_package(PkgConfig QUIET)
if(PKG_CONFIG_FOUND)
pkg_search_module(UUID uuid IMPORTED_TARGET)
if(TARGET PkgConfig::UUID)
message(STATUS "Found libuuid")
set(UUID_FOUND TRUE)
endif()
endif()
- Next, we set up our shared library target and generate the export header, as shown in the previous recipe:
add_library(message-shared SHARED "")
include(GenerateExportHeader)
generate_export_header(message-shared
BASE_NAME "message"
EXPORT_MACRO_NAME "message_EXPORT"
EXPORT_FILE_NAME "${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/messageExport.h"
DEPRECATED_MACRO_NAME "message_DEPRECATED"
NO_EXPORT_MACRO_NAME "message_NO_EXPORT"
STATIC_DEFINE "message_STATIC_DEFINE"
NO_DEPRECATED_MACRO_NAME "message_NO_DEPRECATED"
DEFINE_NO_DEPRECATED
)
target_sources(message-shared
PRIVATE
${CMAKE_CURRENT_LIST_DIR}/Message.cpp
)
- We set PUBLIC and INTERFACE compile definitions for the target. Note the use of the $<INSTALL_INTERFACE:...> generator expression for the latter:
target_compile_definitions(message-shared
PUBLIC
$<$<BOOL:${UUID_FOUND}>:HAVE_UUID>
INTERFACE
$<INSTALL_INTERFACE:USING_message>
)
- Next, the include directories are set. Once again note the use of $<BUILD_INTERFACE:...> and $<INSTALL_INTERFACE:...> generator expressions. We will comment on these later on:
target_include_directories(message-shared
PUBLIC
$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}>
$<INSTALL_INTERFACE:${INSTALL_INCLUDEDIR}>
)
- We finish off the shared library target by listing link libraries and target properties. These are unchanged from the previous recipe:
target_link_libraries(message-shared
PUBLIC
$<$<BOOL:${UUID_FOUND}>:PkgConfig::UUID>
)
set_target_properties(message-shared
PROPERTIES
POSITION_INDEPENDENT_CODE 1
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN 1
SOVERSION ${PROJECT_VERSION_MAJOR}
OUTPUT_NAME "message"
DEBUG_POSTFIX "_d"
PUBLIC_HEADER "Message.hpp;${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/messageExport.h"
MACOSX_RPATH ON
)
The same is done for the message-static library target:
- We first declare it and list its sources:
add_library(message-static STATIC "")
target_sources(message-static
PRIVATE
${CMAKE_CURRENT_LIST_DIR}/Message.cpp
)
- We give PUBLIC and INTERFACE compile definitions, as in the previous recipe, but now using the $<INSTALL_INTERFACE:...> generator expression:
target_compile_definitions(message-static
PUBLIC
message_STATIC_DEFINE
$<$<BOOL:${UUID_FOUND}>:HAVE_UUID>
INTERFACE
$<INSTALL_INTERFACE:USING_message>
)
- We list include directories with the same command used for the shared target:
target_include_directories(message-static
PUBLIC
$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}>
$<INSTALL_INTERFACE:${INSTALL_INCLUDEDIR}>
)
- Link libraries and target properties are unchanged with respect to the previous recipe:
target_link_libraries(message-static
PUBLIC
$<$<BOOL:${UUID_FOUND}>:PkgConfig::UUID>
)
set_target_properties(message-static
PROPERTIES
POSITION_INDEPENDENT_CODE 1
ARCHIVE_OUTPUT_NAME "message"
DEBUG_POSTFIX "_sd"
RELEASE_POSTFIX "_s"
PUBLIC_HEADER "Message.hpp;${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/messageExport.h"
)
- Executables are generated with the exact same commands used in the previous recipe:
add_executable(hello-world_wDSO hello-world.cpp)
target_link_libraries(hello-world_wDSO
PUBLIC
message-shared
)
# Prepare RPATH
file(RELATIVE_PATH _rel ${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR} ${CMAKE_INSTALL_PREFIX})
if(APPLE)
set(_rpath "@loader_path/${_rel}")
else()
set(_rpath "\$ORIGIN/${_rel}")
endif()
file(TO_NATIVE_PATH "${_rpath}/${INSTALL_LIBDIR}" message_RPATH)
set_target_properties(hello-world_wDSO
PROPERTIES
MACOSX_RPATH ON
SKIP_BUILD_RPATH OFF
BUILD_WITH_INSTALL_RPATH OFF
INSTALL_RPATH "${message_RPATH}"
INSTALL_RPATH_USE_LINK_PATH ON
)
add_executable(hello-world_wAR hello-world.cpp)
target_link_libraries(hello-world_wAR
PUBLIC
message-static
)
We are now ready to look at the installation rules:
- We list the installation rules for our targets all together, since CMake can correctly place each of the target in the proper destination. This time, we add the EXPORT keyword so that CMake will generate an exported target file for our targets:
install(
TARGETS
message-shared
message-static
hello-world_wDSO
hello-world_wAR
EXPORT
messageTargets
ARCHIVE
DESTINATION ${INSTALL_LIBDIR}
COMPONENT lib
RUNTIME
DESTINATION ${INSTALL_BINDIR}
COMPONENT bin
LIBRARY
DESTINATION ${INSTALL_LIBDIR}
COMPONENT lib
PUBLIC_HEADER
DESTINATION ${INSTALL_INCLUDEDIR}/message
COMPONENT dev
)
- The auto-generated export target file is called messageTargets.cmake, and we need to explicitly specify install rules for it. The destination of this file is INSTALL_CMAKEDIR defined in the root CMakeLists.txt file:
install(
EXPORT
messageTargets
NAMESPACE
"message::"
DESTINATION
${INSTALL_CMAKEDIR}
COMPONENT
dev
)
- Finally, we need to generate the proper CMake configuration files. These will guarantee that a downstream project will be able to find the targets exported by the message library. To do so, we first include the CMakePackageConfigHelpers.cmake standard module:
include(CMakePackageConfigHelpers)
- We let CMake generate a file containing version information for our library:
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/messageConfigVersion.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
- Using the configure_package_config_file function, we generate the actual CMake configuration file. This is based on the template cmake/messageConfig.cmake.in file:
configure_package_config_file(
${PROJECT_SOURCE_DIR}/cmake/messageConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/messageConfig.cmake
INSTALL_DESTINATION ${INSTALL_CMAKEDIR}
)
- As a last step, we set the install rules for these two auto-generated configuration files:
install(
FILES
${CMAKE_CURRENT_BINARY_DIR}/messageConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/messageConfigVersion.cmake
DESTINATION
${INSTALL_CMAKEDIR}
)
What are the contents of the cmake/messageConfig.cmake.in template file? The header of this file serves as documentation for its users. Let us look at the actual CMake commands:
- We start with a placeholder that will be replaced by the configure_package_config_file command:
@PACKAGE_INIT@
- We include the auto-generated export files for the targets :
include("${CMAKE_CURRENT_LIST_DIR}/messageTargets.cmake")
- Then we check whether the static and shared libraries and the two "Hello, World" executables are present with the check_required_components function provided by CMake:
check_required_components(
"message-shared"
"message-static"
"message-hello-world_wDSO"
"message-hello-world_wAR"
)
- We check whether the target PkgConfig::UUID exists. If not, we search again for the UUID library, but only if we are not on Windows:
if(NOT WIN32)
if(NOT TARGET PkgConfig::UUID)
find_package(PkgConfig REQUIRED QUIET)
pkg_search_module(UUID REQUIRED uuid IMPORTED_TARGET)
endif()
endif()
Let us try this out:
$ mkdir -p build
$ cd build
$ cmake -DCMAKE_INSTALL_PREFIX=$HOME/Software/recipe-03 ..
$ cmake --build . --target install
The install tree has the following structure:
$HOME/Software/recipe-03/
├── bin
│ ├── hello-world_wAR
│ └── hello-world_wDSO
├── include
│ └── message
│ ├── messageExport.h
│ └── Message.hpp
├── lib64
│ ├── libmessage_s.a
│ ├── libmessage.so -> libmessage.so.1
│ └── libmessage.so.1
└── share
└── cmake
└── recipe-03
├── messageConfig.cmake
├── messageConfigVersion.cmake
├── messageTargets.cmake
└── messageTargets-release.cmake
You will notice that a share subdirectory has appeared and it contains all the files that we have asked CMake to autogenerate. From now on, the users of our message library will be able to locate the message library by doing this in their own CMakeLists.txt file, provided that they set the message_DIR CMake variable to point to the share/cmake/message directory in the install tree:
find_package(message 1 CONFIG REQUIRED)