

Introduction:
This note will introduce some basic concepts through a small project, which will also be used in later notes. Installing files, libraries, and executables is a very basic task, but it can also bring some issues. This note demonstrates how to effectively avoid these problems using CMake.

✦
Project Structure
✦
├── CMakeLists.txt
├── src
│ ├── CMakeLists.txt
│ ├── hello_world.cpp
│ ├── message.cpp
│ └── message.hpp
└── test
└── CMakeLists.txt
Project Address
https://gitee.com/jiangli01/tutorials/tree/master/cmake-tutorial/chapter9/01
✦
Related Source Code
✦
src/CMakeLists.txt
# 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()
# SHARED library
add_library(message-shared SHARED "")
target_sources(message-shared
PRIVATE
${CMAKE_CURRENT_LIST_DIR}/message.cpp
)
target_compile_definitions(message-shared
PUBLIC
$<$<BOOL:${UUID_FOUND}>:HAVE_UUID>
)
target_link_libraries(message-shared
PUBLIC
$<$<BOOL:${UUID_FOUND}>:PkgConfig::UUID>
)
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
)
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
)
# <<< Install and export targets >>>
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
)
target_compile_definitions(message-shared
PUBLIC
$<$<BOOL:${UUID_FOUND}>:HAVE_UUID>
)
If we found the third-party library UUID
, this command will add the HAVE_UUID
compile definition to the message shared target and its dependent libraries.
This CMake script uses the set_target_properties
command to set a series of properties for the target message-shared
(which is typically a library or executable). Below I will explain the meaning of these properties in English:
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
)
-
POSITION_INDEPENDENT_CODE 1
: Sets the code to be position-independent. This is important for creating shared libraries, as it allows the code to run at any location in memory, which is necessary for shared libraries. -
SOVERSION ${PROJECT_VERSION_MAJOR}
: Sets the shared object version number, here using the project’s major version number. -
OUTPUT_NAME "message"
: Specifies the output name. Although the target name ismessage-shared
, the file generated during the build will be namedmessage
(for example,message.dll
ormessage.so
). -
DEBUG_POSTFIX "_d"
: Adds a suffix to the output for the debug version. When building the debug version, the output file name will have an additional_d
suffix, helping to distinguish between debug and release versions. -
PUBLIC_HEADER "message.hpp"
: Specifies the public header file. -
MACOSX_RPATH ON
: Enables RPATH on macOS systems. This is a method for setting the dynamic library search path, helping applications find their dependent shared libraries at runtime. -
WINDOWS_EXPORT_ALL_SYMBOLS ON
: Automatically exports all symbols on Windows. This is particularly useful for creating DLLs (dynamic link libraries) as it simplifies the process of exporting symbols.
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()
In this CMake script command, file(RELATIVE_PATH ...)
is used to calculate the relative path between two paths.
The purpose of this command is to find out the relative path from ${CMAKE_INSTALL_PREFIX}
to ${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR}
. In other words, it is looking for the path from the installed binary directory (INSTALL_BINDIR
) to the root directory of the installation (CMAKE_INSTALL_PREFIX
). In most cases, this will simply resolve to a relative path upward from the binary directory (like ../
or more levels of ../../
, depending on the depth of INSTALL_BINDIR
). This type of calculation is very useful when handling installation and packaging, especially when dealing with portability and different system structures. By setting this relative path, it ensures that no matter where your software is installed, the references between files and resources are correct.
This CMake script code uses the file(RELATIVE_PATH ...)
command to calculate a relative path and sets a variable named _rpath
based on the operating system type (Apple system or others) to specify the runtime search path (RPATH) for dynamic libraries. Here is a detailed explanation:
-
if(APPLE)
andelse()
: These two lines of code check whether the current build is on an Apple system (such as macOS). If so, a specific RPATH setting method is used for Apple systems; if not (such as on Linux or Windows), another method is used. -
set(_rpath "@loader_path/${_rel}")
: On Apple systems,_rpath
is set to"@loader_path/${_rel}"
. Here,@loader_path
is a special marker that indicates the location of the executable file that loads the dynamic library. This method allows the dynamic library to be found in a path relative to the executable file. -
set(_rpath "$ORIGIN/${_rel}")
: On non-Apple systems,_rpath
is set to" extbackslash$ORIGIN/${_rel}"
. Here,$ORIGIN
is also a special marker that indicates the location of the executable file that loads the dynamic library. Similar to the method for Apple systems, it allows the dynamic library to be found in a path relative to the executable file.
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
)
Using the set_target_properties
command to set a series of properties for the target named hello-world_wDSO
.
-
MACOSX_RPATH ON
: This option is used to enable RPATH on macOS systems. This means that during the build on macOS, CMake will automatically set the runtime path, which helps the program find its dynamic library dependencies at runtime. -
SKIP_BUILD_RPATH OFF
: When this option is OFF, CMake will use RPATH during the build phase. This ensures that during the build (for example, when running tests), the dynamic libraries can be found. -
BUILD_WITH_INSTALL_RPATH OFF
: This option indicates that the installation RPATH is not used during the build. This means that the RPATH used during the build and the RPATH used after installation are different. Typically, the RPATH during the build points to libraries in the build directory, while the RPATH after installation points to libraries in the installation directory. -
INSTALL_RPATH "${message_RPATH}"
: This option sets the RPATH after installation.${message_RPATH}
is a variable that should be defined elsewhere and contains the path to the runtime libraries (such as dynamic link libraries). This means that oncehello-world_wDSO
is installed, it will use the path specified by this variable to find its runtime dependencies.
-
INSTALL_RPATH_USE_LINK_PATH ON
: When this option is ON, CMake will consider the link paths of targets when setting the RPATH after installation. This means that the RPATH after installation will not only include the paths specified byINSTALL_RPATH
, but also include all paths used when linking the target. This helps ensure that all required dynamic libraries can be found at runtime, especially when these libraries are located in non-standard or non-default locations.
These properties together ensure that hello-world_wDSO
can correctly find its dynamic link library dependencies during build, installation, and runtime. This is a very important part of cross-platform development and deployment of applications, especially when involving dynamic link libraries.
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
)
Using the install()
command to define how to install the two targets message-shared
and hello-world_wDSO
.
-
TARGETS message-shared hello-world_wDSO
: This specifies the targets to install. Here there are two targets:message-shared
andhello-world_wDSO
. -
ARCHIVE DESTINATION ${INSTALL_LIBDIR} COMPONENT lib
: This part specifies the installation location for static libraries (.a
or.lib
files). They will be installed in the directory defined by${INSTALL_LIBDIR}
and marked as part of thelib
component. Typically,${INSTALL_LIBDIR}
points to directories likelib
orlib64
. -
RUNTIME DESTINATION ${INSTALL_BINDIR} COMPONENT bin
: This defines the installation location for executable files (hello-world_wDSO
). They will be installed in the directory specified by${INSTALL_BINDIR}
, typically a directory likebin
. These files are marked as part of thebin
component. -
LIBRARY DESTINATION ${INSTALL_LIBDIR} COMPONENT lib
: This specifies the installation location for dynamic libraries (.so
,.dll
, or.dylib
files). Like static libraries, they will be installed in the${INSTALL_LIBDIR}
directory and marked as part of thelib
component. -
PUBLIC_HEADER DESTINATION ${INSTALL_INCLUDEDIR}/message COMPONENT dev
: This specifies the installation location for public header files (likemessage.hpp
). These files will be installed in the${INSTALL_INCLUDEDIR}/message
directory, typically a directory likeinclude/message
, as part of thedev
component. This makes it easy for other developers to find and use these header files in their own projects.
src/message.h
class Message {
public:
Message(const std::string &m) : message_(m) {}
friend std::ostream &operator<<(std::ostream &os, Message &obj) {
return obj.PrintObject(os);
}
private:
std::string message_;
std::ostream &PrintObject(std::ostream &os);
};
std::string GetUUID();
src/message.cpp
//
// Author: jiangli
// Email: [email protected]
//
#include "message.hpp"
#include <iostream>
#include <string>
#ifdef HAVE_UUID
#include <uuid/uuid.h>
#endif
std::ostream &Message::PrintObject(std::ostream &os) {
os << "This is my very nice message: " << std::endl;
os << message_ << std::endl;
os << "...and here is its UUID: " << GetUUID();
return os;
}
#ifdef HAVE_UUID
std::string GetUUID() {
uuid_t uuid;
uuid_generate(uuid);
char uuid_str[37];
uuid_unparse_lower(uuid, uuid_str);
uuid_clear(uuid);
std::string uuid_cxx(uuid_str);
return uuid_cxx;
}
#else
std::string GetUUID() { return "Ooooops, no UUID for you!"; }
#endif
src/hello_world.cpp
#include <cstdlib>
#include <iostream>
#include "message.hpp"
int main() {
Message say_hello("Hello, CMake World!");
std::cout << say_hello << std::endl;
Message say_goodbye("Goodbye, CMake World");
std::cout << say_goodbye << std::endl;
retu
test/CMakeLists.txt
add_test(
NAME test_shared
COMMAND $<TARGET_FILE:hello-world_wDSO>
)
CMakeLists.txt
cmake_minimum_required(VERSION 3.6 FATAL_ERROR)
project(
example
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)
set(CMAKE_INSTALL_PREFIX ${CMAKE_SOURCE_DIR}/output/)
message(STATUS "Project will be installed to ${CMAKE_INSTALL_PREFIX}")
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}")
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()
add_subdirectory(src)
enable_testing()
add_subdirectory(test)
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")
-
set(INSTALL_LIBDIR ${CMAKE_INSTALL_LIBDIR} CACHE PATH "Installation directory for libraries")
: This line sets a variable namedINSTALL_LIBDIR
that defines the installation directory for library files (both static and dynamic). This directory defaults to the value ofCMAKE_INSTALL_LIBDIR
, which typically points to the system’s default library installation path (likelib
orlib64
). -
set(INSTALL_BINDIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Installation directory for executables")
: This line sets a variable namedINSTALL_BINDIR
to define the installation directory for executable files. This directory defaults to the value ofCMAKE_INSTALL_BINDIR
, which is usually the system’s default installation path for executables (likebin
). -
set(INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "Installation directory for header files")
: This line sets a variable namedINSTALL_INCLUDEDIR
that defines the installation directory for header files. This directory defaults to the value ofCMAKE_INSTALL_INCLUDEDIR
, which typically points to the system’s default installation path for header files (likeinclude
). -
if(WIN32 AND NOT CYGWIN)
andelse()
: These two lines of code are a conditional statement used to distinguish between Windows systems and other systems (such as Linux or macOS).
-
On Windows systems, if not in a Cygwin environment,
DEF_INSTALL_CMAKEDIR
is set toCMake
. This means that CMake configuration files will be installed in a directory namedCMake
. -
On other systems,
DEF_INSTALL_CMAKEDIR
is set toshare/cmake/${PROJECT_NAME}
. Here${PROJECT_NAME}
is the variable for the project name, and this path is typically used to store project-related CMake configuration files.
set(INSTALL_CMAKEDIR ${DEF_INSTALL_CMAKEDIR} CACHE PATH "Installation directory for CMake files")
: This line sets a variable named INSTALL_CMAKEDIR
that defines the installation directory for CMake configuration files, with the value set earlier based on the platform condition in DEF_INSTALL_CMAKEDIR
.
The purpose of these settings is to ensure that the project’s library files, executables, header files, and CMake files can all be installed in appropriate locations across different operating systems and environments. By using these variables, the CMake script can flexibly adapt to different system directory structures and user-customized installation paths. This approach enhances the project’s portability and flexibility, making the build and installation process more consistent and predictable across different environments. Additionally, using cache variables (CACHE PATH
) allows users to override these paths during configuration (the configure phase of CMake), further increasing flexibility.
✦
Results Display
✦
mkdir build & cd build
cmake ..
cmake --build . --target install
The contents of the build directory on GNU/Linux are as follows:
├── build
├── bin
│ └── hello-world_wDSO
├── CMakeCache.txt
├── cmake_install.cmake
├── CTestTestfile.cmake
├── install_manifest.txt
├── lib
│ ├── libmessage.so -> libmessage.so.1
│ └── libmessage.so.1
├── Makefile
├── src
├── test
└── Testing
In the installation location, the following directory structure can be found:
.
├── bin
│ └── hello-world_wDSO
├── include
│ └── message
│ └── message.hpp
└── lib
├── libmessage.so -> libmessage.so.1
└── libmessage.so.1
✦
Supplementary Content
✦
Installing to Standard Locations
What is a good layout for project installation? If only you are using the project, then it doesn’t matter whether the layout is good or bad. However, once you publish the product externally and share the project with others, a reasonable layout should be provided when installing the project.
We can follow some standards, and CMake can help us achieve this. In fact, the GNUInstallDirs.cmake
module does just that by defining a set of variables that are the names of subdirectories for installing different types of files.
-
CMAKE_INSTALL_BINDIR
: Defines the subdirectory where user executable files are located, which is thebin
directory under the selected installation directory. -
CMAKE_INSTALL_LIBDIR
: Expands to the subdirectory where the target code libraries (i.e., static and dynamic libraries) are located. On 64-bit systems, it islib64
, while on 32-bit systems, it is justlib
. -
CMAKE_INSTALL_INCLUDEDIR
: Uses this variable to obtain the correct subdirectory for header files, which isinclude
.
Users may want to override these options. Allowing users to override the installation directories in the main CMakeLists.txt file can be done as follows:
# 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")
Redefines the variables INSTALL_BINDIR
, INSTALL_LIBDIR
, and INSTALL_INCLUDEDIR
used in the project.
When only the library installation is required:
$ cmake -D COMPONENT=lib -P cmake_install.cmake
Correctly setting RPATH
can be quite tricky, but it is unavoidable for users. By default, CMake
sets the RPATH
for executables, assuming they will run from the build tree. However, after installation, the RPATH
is cleared, and when users want to run hello-world_wDSO
, problems can arise. Using the ldd
tool on Linux, we can check the executable file hello-world_wDSO
in the build tree, and running ldd hello-world_wDSO
will yield the following result:
libmessage.so.1 => /home/jiangli/repo/tutorials/cmake-tutorial/chapter9/01/build/lib/libmessage.so.1 (0x00007f43a4df7000)
Running ldd hello-world_wDSO
in the installation directory will yield the following result:
libmessage.so.1 => Not found
This is clearly not acceptable. However, hardcoding the RPATH
to point to the build tree or installation directory is also incorrect: either of these locations may be deleted, leading to corruption of the executable file. The given solution sets different RPATH
for executables in the build tree and installation directory, so it always points to a “meaningful” location; that is, as close as possible to the executable file. Running ldd
in the build tree shows the same output:
libmessage.so.1 => /home/jiangli/repo/tutorials/cmake-tutorial/chapter9/01/output/bin/./../lib/libmessage.so.1 (0x00007f0ebfc4a000)
libmessage.so.1 => /home/jiangli/repo/tutorials/cmake-tutorial/chapter9/01/build/lib/libmessage.so.1 (0x00007f43a4df7000)
Using the CMake install command with a target parameter, which has four additional parameters:
-
FILES
andPROGRAMS
, used to install files or programs, respectively. After installation, appropriate permissions are set for the installed files. For files, read and write permissions are granted to the owner, and read permissions are granted to the group and other users and groups. For programs, execution permissions will be granted. Note thatPROGRAMS
should be used with executables that are not build targets. -
DIRECTORY
, used to install a directory. When only a directory name is given, it is generally understood as relative to the current source directory. The granularity of directory installation can be controlled. -
SCRIPT
, which can be used to define custom installation rules in CMake scripts. -
EXPORT
, this parameter is used to export targets.
Finally, I wish everyone to become stronger!!! If this note has helped you, please like and share!!!🌹🌹🌹
