如何用 CMake 生成 C++ 库(支持 find_package 机制)

参考链接:
bottle/cpp_with_cmake/cmake_static_library

googletest

CMake官方文档:CMakePackageConfigHelpers

CMake要生成一个C++静态库,其实非常简单,只需要如下即可

# CMakeLists.txt

cmake_minimum_required(VERSION 3.16.6)
project(lib)

##################################################################
# 目标配置
##################################################################
set(TARGET_NAME lib)

##################################################################
# 源文件
##################################################################
set(TARGET_SOURCES
    lib.cpp
)
set(TARGET_PUBLIC_HEADERS
    lib.h
)

##################################################################
# 生成库属性
##################################################################
add_library(${TARGET_NAME} STATIC ${TARGET_PUBLIC_HEADERS} ${TARGET_SOURCES})

我们的源文件内容如下

// lib.cpp
#include "lib.h"

int add(int a, int b) {
    return a + b;
}


// lib.h
#pragma once

int add(int a, int b);

 通过以下命令就可以完成构建

 构建结果如下

但是我们需要通过 install 命令安装,需要增加 isntall 到 CMakeLists.txt

# CMakeLists.txt

cmake_minimum_required(VERSION 3.16.6)
project(lib)

##################################################################
# 目标配置
##################################################################
set(TARGET_NAME lib)
set(TARGET_NAMESPACE Library)

set(TARGET_RUNTIME_INSTALL_DIR bin)
set(TARGET_ARCHIVE_INSTALL_DIR lib)
set(TARGET_LIBRARY_INSTALL_DIR lib)
set(TARGET_PUBLIC_HEADER_INSTALL_DIR include)

##################################################################
# 源文件
##################################################################
set(TARGET_SOURCES
    lib.cpp
)
set(TARGET_PUBLIC_HEADERS
    lib.h
)

##################################################################
# 生成库属性
##################################################################
add_library(${TARGET_NAME} STATIC ${TARGET_PUBLIC_HEADERS} ${TARGET_SOURCES})

# 配置头文件,install命令执行时将复制这些配置的头文件
set_target_properties(${TARGET_NAME} PROPERTIES
    PUBLIC_HEADER "${TARGET_PUBLIC_HEADERS}"
)

# 将头文件包含目录配置到 INTERFACE,便于生成的库在被引用时可以自动添加包含目录
string(REPLACE ";" "$<SEMICOLON>" _include_dirs "${TARGET_INCLUDE_DIRS}")
target_include_directories(${TARGET_NAME} SYSTEM INTERFACE
    "$<BUILD_INTERFACE:${_include_dirs}>"
    "$<INSTALL_INTERFACE:$<INSTALL_PREFIX>/${TARGET_PUBLIC_HEADER_INSTALL_DIR}>"
)

##################################################################
# 安装目标
##################################################################
# 配置安装路径
install(
    TARGETS ${TARGET_NAME}
    EXPORT ${TARGET_NAME}
    RUNTIME DESTINATION ${TARGET_RUNTIME_INSTALL_DIR}
    ARCHIVE DESTINATION ${TARGET_ARCHIVE_INSTALL_DIR}
    LIBRARY DESTINATION ${TARGET_LIBRARY_INSTALL_DIR}
    PUBLIC_HEADER DESTINATION ${TARGET_PUBLIC_HEADER_INSTALL_DIR}
    PRIVATE_HEADER DESTINATION ${TARGET_PRIVATE_HEADER_INSTALL_DIR}
)

# 生成 <TARGET>Targets*.cmake 文件
install(
    EXPORT ${TARGET_NAME}
    FILE ${TARGET_NAME}Targets.cmake
    NAMESPACE ${TARGET_NAMESPACE}:: # Defines namespace in XXXTargets.cmake
    DESTINATION lib/cmake/${TARGET_NAME}
)

那么我们重新进行生成,指定 CMAKE_INSTALL_PREFIX,安装到指定临时文件夹

 生成的结果为

将被安装到

我们看看通过 install 命令生成的 <TARGET>Targets*.cmake 内容

# libTargets.cmake

# Generated by CMake

if("${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}" LESS 2.8)
   message(FATAL_ERROR "CMake >= 2.8.0 required")
endif()
if(CMAKE_VERSION VERSION_LESS "2.8.3")
   message(FATAL_ERROR "CMake >= 2.8.3 required")
endif()
cmake_policy(PUSH)
cmake_policy(VERSION 2.8.3...3.22)
#----------------------------------------------------------------
# Generated CMake target import file.
#----------------------------------------------------------------

# Commands may need to know the format version.
set(CMAKE_IMPORT_FILE_VERSION 1)

# Protect against multiple inclusion, which would fail when already imported targets are added once more.
set(_cmake_targets_defined "")
set(_cmake_targets_not_defined "")
set(_cmake_expected_targets "")
foreach(_cmake_expected_target IN ITEMS Library::lib)
  list(APPEND _cmake_expected_targets "${_cmake_expected_target}")
  if(TARGET "${_cmake_expected_target}")
    list(APPEND _cmake_targets_defined "${_cmake_expected_target}")
  else()
    list(APPEND _cmake_targets_not_defined "${_cmake_expected_target}")
  endif()
endforeach()
unset(_cmake_expected_target)
if(_cmake_targets_defined STREQUAL _cmake_expected_targets)
  unset(_cmake_targets_defined)
  unset(_cmake_targets_not_defined)
  unset(_cmake_expected_targets)
  unset(CMAKE_IMPORT_FILE_VERSION)
  cmake_policy(POP)
  return()
endif()
if(NOT _cmake_targets_defined STREQUAL "")
  string(REPLACE ";" ", " _cmake_targets_defined_text "${_cmake_targets_defined}")
  string(REPLACE ";" ", " _cmake_targets_not_defined_text "${_cmake_targets_not_defined}")
  message(FATAL_ERROR "Some (but not all) targets in this export set were already defined.\nTargets Defined: ${_cmake_targets_defined_text}\nTargets not yet defined: ${_cmake_targets_not_defined_text}\n")
endif()
unset(_cmake_targets_defined)
unset(_cmake_targets_not_defined)
unset(_cmake_expected_targets)


# Compute the installation prefix relative to this file.
get_filename_component(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_FILE}" PATH)
get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
if(_IMPORT_PREFIX STREQUAL "/")
  set(_IMPORT_PREFIX "")
endif()

# Create imported target Library::lib
add_library(Library::lib STATIC IMPORTED)

set_target_properties(Library::lib PROPERTIES
  INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
  INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
)

# Load information for each installed configuration.
file(GLOB _cmake_config_files "${CMAKE_CURRENT_LIST_DIR}/libTargets-*.cmake")
foreach(_cmake_config_file IN LISTS _cmake_config_files)
  include("${_cmake_config_file}")
endforeach()
unset(_cmake_config_file)
unset(_cmake_config_files)

# Cleanup temporary variables.
set(_IMPORT_PREFIX)

# Loop over all imported files and verify that they actually exist
foreach(_cmake_target IN LISTS _cmake_import_check_targets)
  foreach(_cmake_file IN LISTS "_cmake_import_check_files_for_${_cmake_target}")
    if(NOT EXISTS "${_cmake_file}")
      message(FATAL_ERROR "The imported target \"${_cmake_target}\" references the file
   \"${_cmake_file}\"
but this file does not exist.  Possible reasons include:
* The file was deleted, renamed, or moved to another location.
* An install or uninstall procedure did not complete successfully.
* The installation package was faulty and contained
   \"${CMAKE_CURRENT_LIST_FILE}\"
but not all the files it references.
")
    endif()
  endforeach()
  unset(_cmake_file)
  unset("_cmake_import_check_files_for_${_cmake_target}")
endforeach()
unset(_cmake_target)
unset(_cmake_import_check_targets)

# This file does not depend on other imported targets which have
# been exported from the same project but in a separate export set.

# Commands beyond this point should not need to know the version.
set(CMAKE_IMPORT_FILE_VERSION)
cmake_policy(POP)
# libTargets-debug.cmake

#----------------------------------------------------------------
# Generated CMake target import file for configuration "Debug".
#----------------------------------------------------------------

# Commands may need to know the format version.
set(CMAKE_IMPORT_FILE_VERSION 1)

# Import target "Library::lib" for configuration "Debug"
set_property(TARGET Library::lib APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(Library::lib PROPERTIES
  IMPORTED_LINK_INTERFACE_LANGUAGES_DEBUG "CXX"
  IMPORTED_LOCATION_DEBUG "${_IMPORT_PREFIX}/lib/lib.lib"
  )

list(APPEND _cmake_import_check_targets Library::lib )
list(APPEND _cmake_import_check_files_for_Library::lib "${_IMPORT_PREFIX}/lib/lib.lib" )

# Commands beyond this point should not need to know the version.
set(CMAKE_IMPORT_FILE_VERSION)

第一个文件 libTargets.cmake,主要就是配置了版本号,并通过 add_library 添加了一个 IMPORTED 类型的静态库,并添加了包含路径


# CMakeLists.txt

string(REPLACE ";" "$<SEMICOLON>" _include_dirs "${TARGET_INCLUDE_DIRS}")
target_include_directories(${TARGET_NAME} SYSTEM INTERFACE
    "$<BUILD_INTERFACE:${_include_dirs}>"
    "$<INSTALL_INTERFACE:$<INSTALL_PREFIX>/${TARGET_PUBLIC_HEADER_INSTALL_DIR}>"
)

# libTargets.cmake 中这一段指定的 INCLUDE 路径,就是通过上面 CMakeLists.txt 的 target_include_directories (INTERFACE) 指定的

set_target_properties(Library::lib PROPERTIES
  INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
  INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
)

 这里如果我们想通过 find_package 去引用此库,会发生以下现象

虽然库是有的,但是 find-package 并不能找到。

那么需要通过 CMakePackageConfigHelpers 来完成配置。在 CMakeLists.txt 末尾增加如下内容

# CMakeLists.txt

##################################################################
# <TARGET>Config.cmake and <TARGET>ConfigVersion.cmake 
##################################################################
# CMakePackageConfigHelpers provides
#   write_basic_package_version_file
#   configure_package_config_file
include(CMakePackageConfigHelpers)

# 生成 <TARGET>ConfigVersion.cmake 文件
write_basic_package_version_file(
    ${TARGET_NAME}ConfigVersion.cmake
    VERSION 1.0.0
    COMPATIBILITY SameMajorVersion # AnyNewerVersion|SameMajorVersion|SameMinorVersion|ExactVersion
)
# 生成 <TARGET>Config.cmake 文件 -- 非常重要,find_package 主要就是搜索该文件
configure_package_config_file(
    PackageConfig.cmake.in ${TARGET_NAME}Config.cmake
    INSTALL_DESTINATION  lib/cmake/${TARGET_NAME}
)

# 安装生成的 <TARGET>Config.cmake 和 <TARGET>ConfigVersion.cmake 文件
install(
    FILES
        ${CMAKE_CURRENT_BINARY_DIR}/${TARGET_NAME}Config.cmake
        ${CMAKE_CURRENT_BINARY_DIR}/${TARGET_NAME}ConfigVersion.cmake
    DESTINATION lib/cmake/${TARGET_NAME}
)

 这里用到了一个输入文件 PackageConfig.cmake.in,其内容如下,主要就是自动生成 libConfig.cmake 的模板,CMake会将 @PACKAGE_INIT@ 部分用默认的模板填充,后面的部分是我们根据自己库的需要指定的。这里我们只增加了一个组件校验,用作示例(其实这个是可以不需要的)。

@PACKAGE_INIT@

include(CMakeFindDependencyMacro)

include("${CMAKE_CURRENT_LIST_DIR}/@TARGET_NAME@Targets.cmake")
check_required_components("@TARGET_NAME@")

好了,那么接下来我们再次生成,得到了如下结果。

这个时候,再用 find_package 来搜索,就可以搜索到 lib 库了。

引用库的 CMakeLists.txt 也非常简单,这里简单贴一下

# 引用库的 CMakeLists.txt

cmake_minimum_required(VERSION 3.16.6)
project(lib_ref_demo)

# 只需要再 CMAKE_PREFIX_PATH 中添加了安装目录即可
# CMake 会按照 .install/lib/cmake/<TargetName>/<TargetName>Config.cmake 这个路径去搜索目标
list(APPEND CMAKE_PREFIX_PATH ${CMAKE_CURRENT_LIST_DIR}/library/.install)

# 通过 find_package 命令搜索我们构建的库
find_package(lib CONFIG REQUIRED)

# 添加 exe 示例程序
add_executable(lib_ref_demo
    ref.cpp
)

# 链接刚刚搜索到的 lib 库
target_link_libraries(lib_ref_demo PUBLIC Library::lib)

 具体的工程内容详见 bottle,当然bottle里面还有动态库的生成。补充说明一下的是,bottle 库中的示例要比这里复杂一丢丢,主要验证了库的后缀指定,include多种结构,public/private 头文件等等细节问题。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值