CMake: Installing a Project

CMake: Installing a Project
CMake: Installing a Project

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.

CMake: Installing a Project

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
)
  1. 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.

  2. SOVERSION ${PROJECT_VERSION_MAJOR}: Sets the shared object version number, here using the project’s major version number.

  3. OUTPUT_NAME "message": Specifies the output name. Although the target name is message-shared, the file generated during the build will be named message (for example, message.dll or message.so).

  4. 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.

  5. PUBLIC_HEADER "message.hpp": Specifies the public header file.

  6. 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.

  7. 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:

  1. if(APPLE) and else(): 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.

  2. 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.

  3. 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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 once hello-world_wDSO is installed, it will use the path specified by this variable to find its runtime dependencies.

  1. 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 by INSTALL_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.

  1. TARGETS message-shared hello-world_wDSO: This specifies the targets to install. Here there are two targets: message-shared and hello-world_wDSO.

  2. 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 the lib component. Typically, ${INSTALL_LIBDIR} points to directories like lib or lib64.

  3. 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 like bin. These files are marked as part of the bin component.

  4. 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 the lib component.

  5. PUBLIC_HEADER DESTINATION ${INSTALL_INCLUDEDIR}/message COMPONENT dev: This specifies the installation location for public header files (like message.hpp). These files will be installed in the ${INSTALL_INCLUDEDIR}/message directory, typically a directory like include/message, as part of the dev 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")
  1. set(INSTALL_LIBDIR ${CMAKE_INSTALL_LIBDIR} CACHE PATH "Installation directory for libraries"): This line sets a variable named INSTALL_LIBDIR that defines the installation directory for library files (both static and dynamic). This directory defaults to the value of CMAKE_INSTALL_LIBDIR, which typically points to the system’s default library installation path (like lib or lib64).

  2. set(INSTALL_BINDIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Installation directory for executables"): This line sets a variable named INSTALL_BINDIR to define the installation directory for executable files. This directory defaults to the value of CMAKE_INSTALL_BINDIR, which is usually the system’s default installation path for executables (like bin).

  3. set(INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "Installation directory for header files"): This line sets a variable named INSTALL_INCLUDEDIR that defines the installation directory for header files. This directory defaults to the value of CMAKE_INSTALL_INCLUDEDIR, which typically points to the system’s default installation path for header files (like include).

  4. if(WIN32 AND NOT CYGWIN) and else(): 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 to CMake. This means that CMake configuration files will be installed in a directory named CMake.

  • On other systems, DEF_INSTALL_CMAKEDIR is set to share/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 the bin 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 is lib64, while on 32-bit systems, it is just lib.

    • CMAKE_INSTALL_INCLUDEDIR: Uses this variable to obtain the correct subdirectory for header files, which is include.

    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 and PROGRAMS, 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 that PROGRAMS 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!!!🌹🌹🌹

    CMake: Installing a Project

    Leave a Comment