CMake: Exporting Libraries Elegantly

CMake: Exporting Libraries ElegantlyCMake: Exporting Libraries Elegantly

Introduction:

In the previous notes (CMake: Detailed Explanation of Static and Dynamic Libraries (Linux/Windows)), we demonstrated how to output dynamic and static libraries. However, there were some issues, such as only outputting header files, symbol tables, and library files. What we actually want is for others to easily find our library when they compile and install it, whether by configuring the environment variables or specifying the library path. This note will show how CMake allows us to export targets so that other CMake projects can easily access them.

CMake: Exporting Libraries Elegantly

Project Structure

.
├── cmake
│   └── messageConfig.cmake.in
├── 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/03

Related Source Code

CMakeLists.txt

# CMake 3.6 needed for IMPORTED_TARGET option
# to pkg_search_module
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)
# Check if the installation prefix has been set
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
    # Set the installation directory to the output folder under the project source directory
    set(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/output/" CACHE PATH "..." FORCE)
endif()

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_BINDDIR})

# 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)

The above CMake has been analyzed in detail in the previous CMake: Exporting Header Files article. Please refer to the previous article. cmake/messageConfig.cmake.in

@PACKAGE_INIT@

include("${CMAKE_CURRENT_LIST_DIR}/messageTargets.cmake")
check_required_components(
  "message-shared"
  "message-static"
  "message-hello-world_wDSO"
  "message-hello-world_wAR"
  )

# Remove dependency on UUID if 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()
@PACKAGE_INIT@

Placeholders will be replaced using the configure_package_config_file command. If the project builds successfully, the replacements will occur in the messageConfig.cmake file:

get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../" ABSOLUTE)

macro(set_and_check _var _file)
  set(${_var} "${_file}")
  if(NOT EXISTS "${_file}")
    message(FATAL_ERROR "File or directory ${_file} referenced by variable ${_var} does not exist !")
  endif()
endmacro()

macro(check_required_components _NAME)
  foreach(comp ${${_NAME}_FIND_COMPONENTS})
    if(NOT ${_NAME}_${comp}_FOUND)
      if(${_NAME}_FIND_REQUIRED_${comp})
        set(${_NAME}_FOUND FALSE)
      endif()
    endif()
  endforeach()
endmacro()
include("${CMAKE_CURRENT_LIST_DIR}/messageTargets.cmake")

Includes the automatically generated export file for targets.

check_required_components(
    "message-shared"
    "message-static"
    "message-hello-world_wDSO"
    "message-hello-world_wAR"
  )

Checks for the static and dynamic libraries, as well as the two Hello, World executables to see if they contain the check_required_components function provided by CMake.

if(NOT WIN32)
  if(NOT TARGET PkgConfig::UUID)
    find_package(PkgConfig REQUIRED QUIET)
    pkg_search_module(UUID REQUIRED uuid IMPORTED_TARGET)
  endif()
endif()

Checks whether the target PkgConfig::UUID exists. If not, it searches for the UUID library again (only valid on non-Windows operating systems). 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()

# <<< Build targets >>>

# SHARED library
add_library(message-shared SHARED "")

include(GenerateExportHeader)
generate_export_header(message-shared
  BASE_NAME "message"
  EXPORT_MACRO_NAME "MESSAGE_LIB_API"
  EXPORT_FILE_NAME "${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/message_export.h"
  STATIC_DEFINE "MESSAGE_STATIC_DEFINE"
  DEFINE_NO_DEPRECATED
  )

target_sources(message-shared
  PRIVATE
    ${CMAKE_CURRENT_LIST_DIR}/message.cpp
  )

target_compile_definitions(message-shared
  PUBLIC
    $<$<BOOL:${UUID_FOUND}>:HAVE_UUID>
  INTERFACE
    $<INSTALL_INTERFACE:USING_MESSAGE>
  )

target_include_directories(message-shared
  PUBLIC
    $<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}>
    $<INSTALL_INTERFACE:${INSTALL_INCLUDEDIR}>
  )

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}/message_export.h"
    MACOSX_RPATH ON
  )

# STATIC library
add_library(message-static STATIC "")

target_sources(message-static
  PRIVATE
    ${CMAKE_CURRENT_LIST_DIR}/message.cpp
  )

target_compile_definitions(message-static
  PUBLIC
    message_STATIC_DEFINE
    $<$<BOOL:${UUID_FOUND}>:HAVE_UUID>
  INTERFACE
    $<INSTALL_INTERFACE:USING_MESSAGE>
  )

target_include_directories(message-static
  PUBLIC
    $<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}>
    $<INSTALL_INTERFACE:${INSTALL_INCLUDEDIR}>
  )

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}/message_export.h"
  )

# EXECUTABLES
add_executable(hello-world_wDSO hello_world.cpp)

target_link_libraries(hello-world_wDSO
  PUBLIC
    message-shared
  )

add_executable(hello-world_wAR hello_world.cpp)

target_link_libraries(hello-world_wAR
  PUBLIC
    message-static
  )

# <<< Install and export 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
  )

install(
  EXPORT
    messageTargets
  NAMESPACE
    "message::"
  DESTINATION
    ${INSTALL_CMAKEDIR}
  COMPONENT
    dev
  )

include(CMakePackageConfigHelpers)
write_basic_package_version_file(
  ${CMAKE_CURRENT_BINARY_DIR}/messageConfigVersion.cmake
  VERSION ${PROJECT_VERSION}
  COMPATIBILITY SameMajorVersion
  )

configure_package_config_file(
  ${PROJECT_SOURCE_DIR}/cmake/messageConfig.cmake.in
  ${CMAKE_CURRENT_BINARY_DIR}/messageConfig.cmake
  INSTALL_DESTINATION ${INSTALL_CMAKEDIR}
  )

install(
  FILES
    ${CMAKE_CURRENT_BINARY_DIR}/messageConfig.cmake
    ${CMAKE_CURRENT_BINARY_DIR}/messageConfigVersion.cmake
  DESTINATION
    ${INSTALL_CMAKEDIR}
  )
install(
  EXPORT
    messageTargets
  NAMESPACE
    "message::"
  DESTINATION
    ${INSTALL_CMAKEDIR}
  COMPONENT
    dev
  )
  • EXPORT messageTargets:

    • This specifies the name of the export set to be installed. messageTargets is a previously defined export set that contains some targets in the project (such as libraries or executables).

    • These targets are defined and built during the CMake build process, and the EXPORT keyword indicates that these targets should be exported as a set during installation. This article packages the dynamic library, static library, and corresponding header files into a set (messageTargets).

  • NAMESPACE "message::":

    • This command sets a namespace prefix for the exported targets. In this case, any exported target will be prefixed with message::.

    • For example, if there is a library named message-shared, it can be referenced in other projects using find_package as message::message-shared.

  • DESTINATION ${INSTALL_CMAKEDIR}:

    • This specifies the target folder where the exported targets (messageTargets) should be installed.

    • ${INSTALL_CMAKEDIR} is a variable defined in the main CMakeLists.txt.

  • COMPONENT dev:

    • The COMPONENT keyword specifies which component this part of the installation belongs to. Here, the component is named dev.

    • In CMake, installations can be divided into multiple components, such as library files, header files, documentation, etc., allowing selective installation of specific components. The dev component may contain development-related files, such as CMake configurations and library files.

include(CMakePackageConfigHelpers)
write_basic_package_version_file(
  ${CMAKE_CURRENT_BINARY_DIR}/messageConfigVersion.cmake
  VERSION ${PROJECT_VERSION}
  COMPATIBILITY SameMajorVersion
  )

configure_package_config_file(
  ${PROJECT_SOURCE_DIR}/cmake/messageConfig.cmake.in
  ${CMAKE_CURRENT_BINARY_DIR}/messageConfig.cmake
  INSTALL_DESTINATION ${INSTALL_CMAKEDIR}
  )

install(
  FILES
    ${CMAKE_CURRENT_BINARY_DIR}/messageConfig.cmake
    ${CMAKE_CURRENT_BINARY_DIR}/messageConfigVersion.cmake
  DESTINATION
    ${INSTALL_CMAKEDIR}
  )
  • include(CMakePackageConfigHelpers):

    • This command includes the CMake module CMakePackageConfigHelpers, which provides some helper macros and functions for generating and configuring package configuration files.

    • These helpers simplify the process of creating configuration files compatible with different projects.

  • write_basic_package_version_file:

    • Generates a package version file containing information about the package version and compatibility.

    • ${CMAKE_CURRENT_BINARY_DIR}/messageConfigVersion.cmake specifies the path and name of the generated file.

    • VERSION ${PROJECT_VERSION} specifies the current version of the package, where ${PROJECT_VERSION} is a variable defined in CMake for the project version.

    • COMPATIBILITY SameMajorVersion specifies the version compatibility rules. In this case, it indicates that the package is compatible with other versions having the same major version number.

  • configure_package_config_file:

    • This command generates a package configuration file from a .in template file.

    • ${PROJECT_SOURCE_DIR}/cmake/messageConfig.cmake.in is the path to the template file, which typically contains configuration information such as library locations, dependencies, etc.

    • ${CMAKE_CURRENT_BINARY_DIR}/messageConfig.cmake is the path and name of the output file.

    • INSTALL_DESTINATION ${INSTALL_CMAKEDIR} specifies the target directory for the configuration file upon installation.

  • The install command:

    • This install command is used to install the previously generated configuration files.

    • The files to be installed are listed after the FILES keyword: the generated messageConfig.cmake and messageConfigVersion.cmake files.

    • DESTINATION ${INSTALL_CMAKEDIR} specifies the installation directory for these files.

src/message.hpp

#pragma once

#include <iosfwd>
#include <string>
#include "message_export.h"
class MESSAGE_LIB_API 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

#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;

  return EXIT_SUCCESS;
}

test/CMakeLists.txt

add_test(
  NAME test_shared
  COMMAND $<TARGET_FILE:hello-world_wDSO>
  )

add_test(
  NAME test_static
  COMMAND $<TARGET_FILE:hello-world_wAR>
  )

Results Display

$ mkdir -p build
$ cd build
$ cmake ..
$ cmake --build . --target install

Installation Tree Structure:

output
   ├── bin
   │   ├── hello-world_wAR
   │   └── hello-world_wDSO
   ├── include
   │   └── message
   │       ├── message_export.h
   │       └── message.hpp
   ├── lib
   │   ├── libmessage_s.a
   │   ├── libmessage.so -> libmessage.so.1
   │   └── libmessage.so.1
   └── share
       └── cmake
           └── example
               ├── messageConfig.cmake
               ├── messageConfigVersion.cmake
               ├── messageTargets.cmake
               └── messageTargets-release.cmake

Finally, I wish everyone to become stronger!!! If this note has helped you, please like and share!!!🌹🌹🌹

CMake: Exporting Libraries ElegantlyCMake: Exporting Libraries Elegantly

Leave a Comment