Let us first go through the root CMakeLists.txt file:
- We start, as usual, by requiring a minimum CMake version and defining a C++11 project. Note that we have set a version for our project with the VERSION keyword to the project command:
# CMake 3.6 needed for IMPORTED_TARGET option
# to pkg_search_module
cmake_minimum_required(VERSION 3.6 FATAL_ERROR)
project(recipe-01
LANGUAGES CXX
VERSION 1.0.0
)
# <<< General set up >>>
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
- The user can define the installation prefix by means of the CMAKE_INSTALL_PREFIX variable. CMake will set a sensible default for this variable: /usr/local on Unix and C:\Program Files on Windows, respectively. We print a status message reporting its value:
message(STATUS "Project will be installed to ${CMAKE_INSTALL_PREFIX}")
- By default, we prefer Release configuration for our project. The user will be able to set this with the CMAKE_BUILD_TYPE variable and we check whether that is the case. If not, we set it ourselves to the default, sensible value:
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
message(STATUS "Build type set to ${CMAKE_BUILD_TYPE}")
- Next we tell CMake where to build the executable, static, and shared library targets. This facilitates access to these build targets in case the user does not intend to actually install the project. We use the standard CMake GNUInstallDirs.cmake module. This will guarantee a sensible and portable project layout:
include(GNUInstallDirs)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY
${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY
${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})
- Whereas the previous commands fixed the location of build output within the build directory, the following are needed to fix the location of executables, libraries, and include files within the install prefix. These will broadly follow the same layout, but we define the new INSTALL_LIBDIR, INSTALL_BINDIR, INSTALL_INCLUDEDIR, and INSTALL_CMAKEDIR variables, which the users can override, if they are so inclined:
# Offer the user the choice of overriding the installation directories
set(INSTALL_LIBDIR ${CMAKE_INSTALL_LIBDIR} CACHE PATH "Installation directory for libraries")
set(INSTALL_BINDIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Installation directory for executables")
set(INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "Installation directory for header files")
if(WIN32 AND NOT CYGWIN)
set(DEF_INSTALL_CMAKEDIR CMake)
else()
set(DEF_INSTALL_CMAKEDIR share/cmake/${PROJECT_NAME})
endif()
set(INSTALL_CMAKEDIR ${DEF_INSTALL_CMAKEDIR} CACHE PATH "Installation directory for CMake files")
- We report the paths where components will be installed to the user:
# Report to user
foreach(p LIB BIN INCLUDE CMAKE)
file(TO_NATIVE_PATH ${CMAKE_INSTALL_PREFIX}/${INSTALL_${p}DIR} _path )
message(STATUS "Installing ${p} components to ${_path}")
unset(_path)
endforeach()
- The last directives in the root CMakeLists.txt file add the src subdirectory, enable testing, and add the tests subdirectory:
add_subdirectory(src)
enable_testing()
add_subdirectory(tests)
We now move on to analyze the src/CMakeLists.txt leaf. This file defines the actual targets to build:
- Our project depends on the UUID library. As shown in Chapter 5, Configure-time and Build-time Operations, Recipe 8, Probing execution, we can find it with the following snippet:
# 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()
- We wish to build a shared library out of our sources and we declare a target called message-shared:
add_library(message-shared SHARED "")
- The sources for this target are specified with the target_sources command:
target_sources(message-shared
PRIVATE
${CMAKE_CURRENT_LIST_DIR}/Message.cpp
)
- We declare compile definitions and link libraries for our target. Note that all are PUBLIC, to ensure that all dependent targets will inherit them properly:
target_compile_definitions(message-shared
PUBLIC
$<$<BOOL:${UUID_FOUND}>:HAVE_UUID>
)
target_link_libraries(message-shared
PUBLIC
$<$<BOOL:${UUID_FOUND}>:PkgConfig::UUID>
)
- Then we set additional properties of our target. We will comment upon these shortly.
set_target_properties(message-shared
PROPERTIES
POSITION_INDEPENDENT_CODE 1
SOVERSION ${PROJECT_VERSION_MAJOR}
OUTPUT_NAME "message"
DEBUG_POSTFIX "_d"
PUBLIC_HEADER "Message.hpp"
MACOSX_RPATH ON
WINDOWS_EXPORT_ALL_SYMBOLS ON
)
- Finally, we add an executable target for our "Hello, world" program:
add_executable(hello-world_wDSO hello-world.cpp)
- The hello-world_wDSO executable target is linked against the shared library:
target_link_libraries(hello-world_wDSO
PUBLIC
message-shared
)
The src/CMakeLists.txt file contains also the installation directives. Before considering these, we need to fix the RPATH for our executable:
- With CMake path manipulations, we set the message_RPATH variable. This will set RPATH appropriately for GNU/Linux and macOS:
# 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)
- We can now use this variable to fix the RPATH for our executable target, hello-world_wDSO. This is achieved by means of a target property. We are also setting additional properties, and we will comment more on these in a moment:
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
)
- We are finally ready to install our library, header, and executable! We use the install command offered by CMake to specify where these should go. Note that the paths are relative; we will elaborate more on this point further below:
install(
TARGETS
message-shared
hello-world_wDSO
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 CMakeLists.txt file in the tests directory contains simple directives to ensure that the "Hello, World" executable runs correctly:
add_test(
NAME test_shared
COMMAND $<TARGET_FILE:hello-world_wDSO>
)
Let us now configure, build, and install the project and look at the result. As soon as any installation directives are added, CMake generates a new target called install that will run the installation rules:
$ mkdir -p build
$ cd build
$ cmake -G"Unix Makefiles" -DCMAKE_INSTALL_PREFIX=$HOME/Software/recipe-01
$ cmake --build . --target install
The contents of the build directory on GNU/Linux will be the following:
build
├── bin
│ └── hello-world_wDSO
├── CMakeCache.txt
├── CMakeFiles
├── cmake_install.cmake
├── CTestTestfile.cmake
├── install_manifest.txt
├── lib64
│ ├── libmessage.so -> libmessage.so.1
│ └── libmessage.so.1
├── Makefile
├── src
├── Testing
└── tests
One the other hand, at the install prefix, you can find the following structure:
$HOME/Software/recipe-01/
├── bin
│ └── hello-world_wDSO
├── include
│ └── message
│ └── Message.hpp
└── lib64
├── libmessage.so -> libmessage.so.1
└── libmessage.so.1
This means that the locations given in the installation directives are relative to the CMAKE_INSTALL_PREFIX instance given by the user.