We will start by looking at the root CMakeLists.txt file, which is coordinating the superbuild:
- Its preamble is unchanged with respect to the previous recipes. We start by declaring a C++11 project, we set a sensible default install prefix, build type, output directories for our targets, and layout of components in the install tree:
cmake_minimum_required(VERSION 3.6 FATAL_ERROR)
project(recipe-04
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)
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}")
message(STATUS "Project will be installed to ${CMAKE_INSTALL_PREFIX}")
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})
# 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")
# 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()
- We set the EP_BASE directory property. This will set the layout for the subprojects in the superbuild. All subprojects will be checked out and built under the subprojects folder of CMAKE_BINARY_DIR:
set_property(DIRECTORY PROPERTY EP_BASE ${CMAKE_BINARY_DIR}/subprojects)
- We then declare the STAGED_INSTALL_PREFIX variable. This variable points to the stage subdirectory under the build directory. The project will be installed here during the build. This is a way of sandboxing the installation process and gives us a chance to check that the whole superbuild will install with a correct layout:
set(STAGED_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/stage)
message(STATUS "${PROJECT_NAME} staged install: ${STAGED_INSTALL_PREFIX}")
- We add the external/upstream subdirectory. This contains CMake instructions to manage our upstream dependencies, in our case, the message library:
add_subdirectory(external/upstream)
- We then include the ExternalProject.cmake standard module:
include(ExternalProject)
- We add our own project as an external project, invoking the ExternalProject_Add command. The SOURCE_DIR option specifies that the sources are in the src subdirectory. We also pass all appropriate CMake arguments to configure our project. Note the use of STAGED_INSTALL_PREFIX as the installation prefix for the subproject:
ExternalProject_Add(${PROJECT_NAME}_core
DEPENDS
message_external
SOURCE_DIR
${CMAKE_CURRENT_SOURCE_DIR}/src
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${STAGED_INSTALL_PREFIX}
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}
-DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD}
-DCMAKE_CXX_EXTENSIONS=${CMAKE_CXX_EXTENSIONS}
-DCMAKE_CXX_STANDARD_REQUIRED=${CMAKE_CXX_STANDARD_REQUIRED}
-Dmessage_DIR=${message_DIR}
CMAKE_CACHE_ARGS
-DCMAKE_PREFIX_PATH:PATH=${CMAKE_PREFIX_PATH}
BUILD_ALWAYS
1
)
- We now add a test for the use_message executable, built by the recipe-04_core target. This will run the staged installation of the use_message executable, that is the one located within the build tree:
enable_testing()
add_test(
NAME
check_use_message
COMMAND
${STAGED_INSTALL_PREFIX}/${INSTALL_BINDIR}/use_message
)
- Finally, we can declare install rules. This time they are rather simple. Since everything needed has already been installed with the correct layout in the staging area, we only need to copy the whole contents of the staging area to the install prefix:
install(
DIRECTORY
${STAGED_INSTALL_PREFIX}/
DESTINATION
.
USE_SOURCE_PERMISSIONS
)
- We declare an additional installation rule with the SCRIPT argument. The CMake script install_hook.cmake will be executed, but only on GNU/Linux and macOS. This script will print the RPATH of the installed executable and run it. We will discuss this in more detail in the next section:
if(UNIX)
set(PRINT_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/cmake/print_rpath.py")
configure_file(cmake/install_hook.cmake.in install_hook.cmake @ONLY)
install(
SCRIPT
${CMAKE_CURRENT_BINARY_DIR}/install_hook.cmake
)
endif()
You will have noticed that -Dmessage_DIR=${message_DIR} has been passed as a CMake argument to our own project. This will correctly set the location of the message library dependency. The value of message_DIR is defined in the CMakeLists.txt file under the external/upstream/message directory. This file handles the dependency on the message library – let us see how:
- We first attempt to find the package. Possibly, the user will have already installed it somewhere on the system and may have passed the message_DIR option when configuring:
find_package(message 1 CONFIG QUIET)
- If that was indeed the case and message was found, we report the location and the version of the target to the user and add a dummy message_external target. The dummy target is needed to handle superbuild dependencies correctly:
if(message_FOUND)
get_property(_loc TARGET message::message-shared PROPERTY LOCATION)
message(STATUS "Found message: ${_loc} (found version ${message_VERSION})")
add_library(message_external INTERFACE) # dummy
- If the library was not found, we will add it as an external project, download it from its online Git repository, and compile it. The install prefix, build type, and installation directories layout are all set from the root CMakeLists.txt file, as are the C++ compiler and flags. The project will be installed to STAGED_INSTALL_PREFIX and then tested:
else()
include(ExternalProject)
message(STATUS "Suitable message could not be located, Building message instead.")
ExternalProject_Add(message_external
GIT_REPOSITORY
https://github.com/dev-cafe/message.git
GIT_TAG
master
UPDATE_COMMAND
""
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${STAGED_INSTALL_PREFIX}
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
CMAKE_CACHE_ARGS
-DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS}
TEST_AFTER_INSTALL
1
DOWNLOAD_NO_PROGRESS
1
LOG_CONFIGURE
1
LOG_BUILD
1
LOG_INSTALL
1
)
- Finally, we set the message_DIR directory to point to the location of the freshly built messageConfig.cmake file. Note that the path is saved to the CMake cache:
if(WIN32 AND NOT CYGWIN)
set(DEF_message_DIR ${STAGED_INSTALL_PREFIX}/CMake)
else()
set(DEF_message_DIR ${STAGED_INSTALL_PREFIX}/share/cmake/message)
endif()
file(TO_NATIVE_PATH "${DEF_message_DIR}" DEF_message_DIR)
set(message_DIR ${DEF_message_DIR}
CACHE PATH "Path to internally built messageConfig.cmake" FORCE)
endif()
We are finally ready to compile our own project and successfully link it against the message library, be it already available on the system or freshly built for the purpose. Since this is a superbuild, the code under the src subdirectory is a fully standalone CMake project:
- We declare a C++11 project, as usual:
cmake_minimum_required(VERSION 3.6 FATAL_ERROR)
project(recipe-04_core
LANGUAGES CXX
)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(GNUInstallDirs)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})
- We attempt finding the message library. Within our superbuild, the configuration will have message_DIR correctly set:
find_package(message 1 CONFIG REQUIRED)
get_property(_loc TARGET message::message-shared PROPERTY LOCATION)
message(STATUS "Found message: ${_loc} (found version ${message_VERSION})")
- We are ready to add our executable target, use_message. This is built from the use_message.cpp source file and links in the message::message-shared target:
add_executable(use_message use_message.cpp)
target_link_libraries(use_message
PUBLIC
message::message-shared
)
- Target properties are set for use_message. Note once again the RPATH fixing:
# Prepare RPATH
file(RELATIVE_PATH _rel ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR} ${CMAKE_INSTALL_PREFIX})
if(APPLE)
set(_rpath "@loader_path/${_rel}")
else()
set(_rpath "\$ORIGIN/${_rel}")
endif()
file(TO_NATIVE_PATH "${_rpath}/${CMAKE_INSTALL_LIBDIR}" use_message_RPATH)
set_target_properties(use_message
PROPERTIES
MACOSX_RPATH ON
SKIP_BUILD_RPATH OFF
BUILD_WITH_INSTALL_RPATH OFF
INSTALL_RPATH "${use_message_RPATH}"
INSTALL_RPATH_USE_LINK_PATH ON
)
- Finally, we set install rules for the use_message target:
install(
TARGETS
use_message
RUNTIME
DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT bin
)
Let us now look at the contents of the install_hook.cmake.in template CMake script:
- The CMake script is executed outside the scope of our main project and hence has no notion of variables or targets defined there. We thus set a variable holding the full path to the installed use_message executable. Note the use of @INSTALL_BINDIR@, which will be resolved by configure_file:
set(_executable ${CMAKE_INSTALL_PREFIX}/@INSTALL_BINDIR@/use_message)
- We need to find the executable for the platform-native tool we will use to print the RPATH of the installed executable. We will search for chrpath, patchelf, and otool. The search exits as soon one is found to be installed with an helpful status message to the user:
set(_patcher)
list(APPEND _patchers chrpath patchelf otool)
foreach(p IN LISTS _patchers)
find_program(${p}_FOUND
NAMES
${p}
)
if(${p}_FOUND)
set(_patcher ${p})
message(STATUS "ELF patching tool ${_patcher} FOUND")
break()
endif()
endforeach()
- We check whether the _patcher variable is not empty. It would mean no ELF patching tool is available and the operation we want to carry out will fail. We emit a fatal error and alert the user that one of the ELF patching tools needs to be installed:
if(NOT _patcher)
message(FATAL_ERROR "ELF patching tool NOT FOUND!\nPlease install one of chrpath, patchelf or otool")
- In case one the ELF patching tools was found, we proceed. We invoke the print_rpath.py Python script, passing the _executable variable as argument. We use execute_process for this purpose:
find_package(PythonInterp REQUIRED QUIET)
execute_process(
COMMAND
${PYTHON_EXECUTABLE} @PRINT_SCRIPT@ "${_patcher}"
"${_executable}"
RESULT_VARIABLE _res
OUTPUT_VARIABLE _out
ERROR_VARIABLE _err
OUTPUT_STRIP_TRAILING_WHITESPACE
)
- We check the _res variable for return code. If execution was successful, we print the standard output stream captured in the _out variable. Otherwise, we print the captured standard output and error streams before exiting with a fatal error:
if(_res EQUAL 0)
message(STATUS "RPATH for ${_executable} is ${_out}")
else()
message(STATUS "Something went wrong!")
message(STATUS "Standard output from print_rpath.py: ${_out}")
message(STATUS "Standard error from print_rpath.py: ${_err}")
message(FATAL_ERROR "${_patcher} could NOT obtain RPATH for ${_executable}")
endif()
endif()
- We invoke once again execute_process to run the installed use_message executable:
execute_process(
COMMAND ${_executable}
RESULT_VARIABLE _res
OUTPUT_VARIABLE _out
ERROR_VARIABLE _err
OUTPUT_STRIP_TRAILING_WHITESPACE
)
- Finally, we report to the user on the result of execute_process:
if(_res EQUAL 0)
message(STATUS "Running ${_executable}:\n ${_out}")
else()
message(STATUS "Something went wrong!")
message(STATUS "Standard output from running ${_executable}:\n ${_out}")
message(STATUS "Standard error from running ${_executable}:\n ${_err}")
message(FATAL_ERROR "Something went wrong with ${_executable}")
endif()