代码:melonedo/cmake-lecture/export

生成库

目录结构:

eval_add
├──CMakeLists.txt
├──cmake
│  ├──installer.cmake
│  └──EvalAddConfig.cmake
├──include
│  └──eval_add
│     └──eval_add.h
└──source
   └──eval_add.cpp

为了保证安装目录/usr/include头文件不会重名,可以给自己的项目单独建一个文件夹。这也反应在了目录结构中,即include下是eval_add,之后才是头文件。

这里简单写一个依赖boost的库,头文件没有包含boost相关,所以Boost::boost也是private。

### eval_add/CMakeLists.txt ###
cmake_minimum_required(VERSION 3.13)
# 少写个C语言省点配置的时间
project(eval_add VERSION 0.0.1 LANGUAGES CXX)

# 提供CMAKE_INSTALL_INCLUDEDIR
include(GNUInstallDirs)
# 外部依赖
find_package(Boost REQUIRED COMPONENTS regex)

# 定义库,没什么特别的
add_library(eval_add SHARED source/eval_add.cpp)
target_link_libraries(eval_add PRIVATE Boost::dynamic_linking Boost::boost Boost::regex Boost::diagnostic_definitions)
if(MSVC)
    target_compile_options(eval_add PRIVATE $<BUILD_INTERFACE:/utf-8>)
endif()

# 需要区分编译和安装,安装后包含路径只是一个简单的include
target_include_directories(eval_add
  PUBLIC
    $<BUILD_INTERFACE:${eval_add_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)

set_target_properties(eval_add PROPERTIES
    CXX_STANDARD 11
    WINDOWS_EXPORT_ALL_SYMBOLS ON # 懒得写__declspec
    DEBUG_POSTFIX -debug # 添加一个后缀区分不同的configuration
)
# 见下
include(cmake/installer.cmake)

导出库

为了让别人使用自己的库,需要把当前的目标的依赖和各种编译要求导出到文件中,随着二进制文件和头文件一起打包。需要安装的内容包括:

  • 本项目的CMake文件XXXConfig.cmake。
  • 本项目的版本XXXConfigVersion.cmake。
  • 导出的目标的二进制文件。
  • 导出的目标的说明XXXTargets.cmake。实际上会根据configuration再生成一个XXXTargets-Relase/Debug.cmake。
  • 导出的变量写在XXXConfig.cmake。
  • 其他文件,如头文件、版权许可。
### eval_add/cmake/installer.cmake ###
include(GNUInstallDirs)
# 版本号为项目版本,生成版本文件
include(CMakePackageConfigHelpers)
write_basic_package_version_file(EvalAddConfigVersion.cmake
    VERSION ${eval_add_VERSION}
    COMPATIBILITY SameMajorVersion)
# 安装XXXConfig.cmake(见下)和对应的版本文件
install(FILES
    ${CMAKE_CURRENT_SOURCE_DIR}/cmake/EvalAddConfig.cmake
    ${CMAKE_CURRENT_BINARY_DIR}/EvalAddConfigVersion.cmake
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/EvalAdd)
# 安装生成的二进制文件,用默认配置即可
install(TARGETS eval_add EXPORT EvalAddTargets)
# 安装本项目的目标
install(EXPORT EvalAddTargets
    FILE EvalAddTargets.cmake
    NAMESPACE EvalAdd:: # 命名空间
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/EvalAdd)
# 安装头文件
install(DIRECTORY include/eval_add TYPE INCLUDE)

其中安装头文件的地方也可以通过设置目标的PUBLIC_HEADER属性实现,不过看起来似乎设计上不是通用的。

这个库的Config.cmake需要手写,内容是找到依赖以及包含上面的目标

### eval_add/cmake/EvalAddConfig.cmake ###
include(CMakeFindDependencyMacro)
# 似乎不需要写REQUIRED
find_dependency(Boost COMPONENTS regex)
include("${CMAKE_CURRENT_LIST_DIR}/EvalAddTargets.cmake")

导出变量

如果要导出变量,为了保证不出现绝对路径,需要使用configure_package_config_file来处理相对路径。首先写模板:

### eval_add/cmake/EvalAddConfig.cmake.in ###
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
find_dependency(Boost COMPONENTS regex)
include("${CMAKE_CURRENT_LIST_DIR}/EvalAddTargets.cmake")
set_and_check(EvalAdd_INCLUDE_DIR "@PACKAGE_CMAKE_INSTALL_INCLUDEDIR@")
check_required_components(EvalAdd)

为了把上面@PACKAGE_var@替换成var相对安装目录的路径,installer.cmake中不能直接用install(FILES)安装cmake/EvalAddConfig.cmake,而是需要先配置

configure_package_config_file(cmake/EvalAddConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/EvalAddConfig.cmake # 和下面保持一致
    INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/EvalAdd # 和下面保持一致
    PATH_VARS CMAKE_INSTALL_INCLUDEDIR)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/EvalAddConfigVersion.cmake
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/EvalAdd)

命令

cmake -B build -DCMAKE_INSTALL_PREFIX=/eval_add/root -DBOOST_ROOT=/boost/root
cmake --build build --target install

使用库

# $<TARGET_RUNTIME_DLLS:target>需要3.21
cmake_minimum_required(VERSION 3.21 REQUIRED)
project(use_eval_add VERSION 0.0.1 LANGUAGES CXX)

# 找到上面的库,注意此时Boost也需要找到,同样要设置BOOST_ROOT等变量
find_package(EvalAdd REQUIRED)

add_executable(main main.cpp)
# 这样就自动处理好了所有的间接依赖、包含路径、编译选项等
target_link_libraries(main EvalAdd::eval_add)

## 接下来是安装可执行文件

# 如果不执行后面的安装,把dll复制到生成目录,方便调试
add_custom_command(TARGET main POST_BUILD COMMAND_EXPAND_LISTS
    COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_RUNTIME_DLLS:main> $<TARGET_FILE_DIR:main>)

# 提供一个直接运行程序的方法:cmake --build build --target run-main
add_custom_target(run-main main)

# 安装应用
include(GNUInstallDirs)
# 默认就好
install(TARGETS main)
# 不确定怎么做,linux上设置一下RPATH似乎好点
set_target_properties(main PROPERTIES INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR})
# 同样,可以复制dll到安装目录
install(FILES $<TARGET_RUNTIME_DLLS:main> TYPE BIN)

命令

cmake -B build -DEvalAdd_ROOT=/eval_add/root -DBOOST_ROOT=/boost/root
cmake --build build --target run-main
cmake --build build --target install

参考

Effective CMake: 〔YouTube〕 〔讲义〕

CMake + Conan: 3 Years Later: 〔Youtube〕 〔讲义〕