目录

CMake 工程化构建与依赖管理

描述工程搭建中常用的 CMake函数,及基本概念。

CMake 以 “目标” 为核心,所有构建产物(可执行文件、库)均通过目标管理。常见目标类型:

  • 可执行目标:add_executable(app src/main.cpp)

  • 库目标:

    add_library(<name> [<type>] [EXCLUDE_FROM_ALL] <sources>...)

    • 静态库/动态库:STATIC/SHARED
  • 对象库:OBJECT,适用于编译对象文件,但不生成单独的库文件

  • 接口库:INTERFACE,无实体文件,适用于传递依赖关系

  • 导入库:IMPORTED,适用于链接外部库

CMake 中存在两种库的使用方式,其本质和用法有显著差异:

类型 本质 使用方式 示例
add_library 创建的库 CMake 目标(Target) 直接使用目标名,无需 ${} target_link_libraries(app PRIVATE CURL_LIB)
find_library 找到的库 库文件路径(字符串) 需用 ${} 引用路径变量 target_link_libraries(app PRIVATE ${CURL_LIB})
  1. 存在形式
    • add 创建的库是 CMake 中的 “目标对象”,包含路径、头文件、依赖等完整信息。
    • find 找到的库是 “字符串路径”(如 /usr/lib/libcurl.so),仅表示文件位置。
  2. 依赖传递
    • add 创建的库可通过 INTERFACE_* 属性自动传递头文件和依赖(如 INTERFACE_INCLUDE_DIRECTORIES)。
    • find 找到的库需手动传递头文件路径(如 include_directories(${CURL_INCLUDE}))。
  3. 使用语法
    • 目标库直接写名称:target_link_libraries(app PRIVATE MBEDTLS_SHARED_LIB)
    • 路径库需用 ${} 展开:target_link_libraries(app PRIVATE ${CURL_LIB_PATH})

通过 set_target_properties 配置目标细节,核心属性包括:

属性 作用 示例
IMPORTED_LOCATION 导入库的文件路径(仅导入目标) IMPORTED_LOCATION “/path/libcurl.so”
INTERFACE_INCLUDE_DIRECTORIES 传递头文件路径(依赖此目标的下游自动获得) INTERFACE_INCLUDE_DIRECTORIES “${MBEDTLS_INCLUDE_DIR}”
INTERFACE_LINK_LIBRARIES 传递链接依赖(下游自动链接) INTERFACE_LINK_LIBRARIES MBEDTLS_CRYPTO_SHARED_LIB

案例 1:定义外部导入库(将 find 到的库转为目标)

# 1. 先用 find 找到库路径(字符串)
find_library(CURL_LIB_PATH libcurl.so HINTS /opt/curl/lib)
find_path(CURL_INCLUDE_DIR curl/curl.h HINTS /opt/curl/include)

# 2. 转为 CMake 目标(方便依赖传递)
add_library(CURL_SHARED_LIB SHARED IMPORTED)
set_target_properties(CURL_SHARED_LIB PROPERTIES
    IMPORTED_LOCATION "${CURL_LIB_PATH}"  # 关联路径
    INTERFACE_INCLUDE_DIRECTORIES "${CURL_INCLUDE_DIR}"  # 自动传递头文件
)

# 3. 使用时直接用目标名(无需 ${})
target_link_libraries(novel_cli PRIVATE CURL_SHARED_LIB)

案例 2:直接使用 find 到的路径(不转为目标)

# 3. 链接时需用 ${} 展开路径
target_link_libraries(novel_cli PRIVATE ${CURL_LIB_PATH})

用于建立目标间的依赖关系,支持三种可见性关键字,控制依赖传递范围:

关键字 含义(以 A 链接 B 为例) 传递性(依赖 A 的 C 是否依赖 B)
PRIVATE B 是 A 的内部依赖(A 的接口不依赖 B) 不传递(C 看不到 B)
PUBLIC B 是 A 的公共依赖(A 的接口依赖 B) 传递(C 自动依赖 B)
INTERFACE A 不依赖 B,但依赖 A 的 C 必须依赖 B 传递(C 自动依赖 B)

mbedtls 库间依赖关系:mbedx509 → mbedtls → mbedcrypto,需通过 INTERFACE_LINK_LIBRARIES 传递依赖:

# 1. 最底层:mbedcrypto(无依赖)
add_library(MBEDTLS_CRYPTO_SHARED_LIB SHARED IMPORTED)
set_target_properties(MBEDTLS_CRYPTO_SHARED_LIB PROPERTIES
    IMPORTED_LOCATION "${MBEDTLS_CRYPTO_LIB_PATH}"
    INTERFACE_INCLUDE_DIRECTORIES "${MBEDTLS_INCLUDE_DIR}"
)

# 2. 中间层:mbedtls(依赖 mbedcrypto)
add_library(MBEDTLS_CORE_SHARED_LIB SHARED IMPORTED)
set_target_properties(MBEDTLS_CORE_SHARED_LIB PROPERTIES
    IMPORTED_LOCATION "${MBEDTLS_CORE_LIB_PATH}"
    INTERFACE_INCLUDE_DIRECTORIES "${MBEDTLS_INCLUDE_DIR}"
    INTERFACE_LINK_LIBRARIES MBEDTLS_CRYPTO_SHARED_LIB  # 传递依赖
)

# 3. 上层:mbedx509(依赖 mbedtls)
add_library(MBEDTLS_X509_SHARED_LIB SHARED IMPORTED)
set_target_properties(MBEDTLS_X509_SHARED_LIB PROPERTIES
    IMPORTED_LOCATION "${MBEDTLS_X509_LIB_PATH}"
    INTERFACE_INCLUDE_DIRECTORIES "${MBEDTLS_INCLUDE_DIR}"
    INTERFACE_LINK_LIBRARIES MBEDTLS_CORE_SHARED_LIB  # 传递依赖
)

下游使用:仅需链接上层库 MBEDTLS_X509_SHARED_LIB,依赖自动传递

add_executable(novel_cli src/main.cpp)
target_link_libraries(novel_cli PRIVATE MBEDTLS_X509_SHARED_LIB)  # 自动依赖 mbedtls + mbedcrypto
方式 作用范围 优缺点
include_directories 全局(当前 CMakeLists 及子目录) 简单但易污染,不推荐大型项目
INTERFACE_INCLUDE_DIRECTORIES 仅传递给依赖此目标的下游 精细化控制,无全局污染,现代 CMake 推荐

错误案例:全局头文件导致冲突

include_directories(${MBEDTLS_INCLUDE_DIR})  # 所有目标都能看到,可能与其他库头文件冲突

正确案例:通过目标传递头文件

# 仅依赖 MBEDTLS_SHARED_LIB 的目标能获得头文件路径
set_target_properties(MBEDTLS_SHARED_LIB PROPERTIES
    INTERFACE_INCLUDE_DIRECTORIES "${MBEDTLS_INCLUDE_DIR}"
)

# novel_cli 链接后自动获得头文件路径,无需全局配置
target_link_libraries(novel_cli PRIVATE MBEDTLS_SHARED_LIB)

若代码所在目标未链接依赖库,头文件路径无法传递。例如:

# 错误:代码在 module-main-unit 目标,但未链接 MBEDTLS_SHARED_LIB
add_library(module-main-unit OBJECT src/hash.cpp)  # hash.cpp 包含 #include "mbedtls/sha256.h"
add_executable(novel_cli $<TARGET_OBJECTS:module-main-unit>)
target_link_libraries(novel_cli PRIVATE MBEDTLS_SHARED_LIB)  # 头文件路径未传递到 module-main-unit

# 正确:代码所在目标需直接链接
target_link_libraries(module-main-unit PRIVATE MBEDTLS_SHARED_LIB)

用于安装目标产物,按类型指定路径:

参数 适用目标 示例路径
RUNTIME 可执行文件(.exe、二进制) ${INSTALL_PATH}/bin
LIBRARY 动态库(.so、.dylib) ${INSTALL_PATH}/lib
ARCHIVE 静态库(.a、.lib) ${INSTALL_PATH}/lib
PUBLIC_HEADER 库的公共头文件 ${INSTALL_PATH}/include

案例:安装可执行文件与本地构建库

set(INSTALL_PATH "/opt/novel_cli")

# 安装可执行文件
install(TARGETS novel_cli
    RUNTIME DESTINATION ${INSTALL_PATH}/bin  # Linux/macOS 二进制,Windows .exe
)

# 安装本地构建的动态库(如自定义的 net 库)
install(TARGETS net
    LIBRARY DESTINATION ${INSTALL_PATH}/lib
)

外部导入库(如通过 find_library 找到的 libcurl.so)无法通过 install(TARGETS) 自动安装,需用 install(FILES) 手动指定:

# 安装外部 CURL 库
install(FILES "${CURL_LIB_PATH}"
    DESTINATION ${INSTALL_PATH}/lib
    PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE  # 设置权限
)

# 安装外部库头文件
install(DIRECTORY "${CURL_INCLUDE_DIR}/curl"
    DESTINATION ${INSTALL_PATH}/include  # 安装到 include/curl 目录
    FILES_MATCHING PATTERN "*.h"         # 仅安装 .h 文件
)

通过脚本自动收集依赖的外部库并安装,避免手动逐个列出:

# 自动收集目标依赖的外部导入库
function(collect_imported_libs target result)
    get_target_property(libs ${target} LINK_LIBRARIES)
    set(imported_libs "")
    foreach(lib ${libs})
        if(TARGET ${lib} AND GET_TARGET_PROPERTY(is_imported ${lib} IMPORTED))
            get_target_property(lib_path ${lib} IMPORTED_LOCATION)
            list(APPEND imported_libs ${lib_path})
        endif()
    endforeach()
    set(${result} ${imported_libs} PARENT_SCOPE)
endfunction()

# 自动安装
collect_imported_libs(novel_cli imported_libs)
if(imported_libs)
    install(FILES ${imported_libs} DESTINATION ${INSTALL_PATH}/lib)
endif()
  • PUBLIC 依赖:编译时传递头文件,安装时自动传递依赖,但可能暴露内部实现。
  • PRIVATE 依赖:隐藏内部实现,不传递头文件,但需额外处理安装依赖。

平衡方案PRIVATE 保留私密性 + $ 确保安装传递

# module-serv-fetch 私有依赖 CURL(编译时不传递),但安装时需传递
target_link_libraries(module-serv-fetch
    PRIVATE CURL_SHARED_LIB
    INTERFACE $<INSTALL_INTERFACE:CURL_SHARED_LIB>  # 安装时下游自动安装 CURL
)

# 下游链接时无需关注依赖,安装时自动包含 CURL
add_executable(novel_cli src/main.cpp)
target_link_libraries(novel_cli PRIVATE module-serv-fetch)
install(TARGETS novel_cli RUNTIME DESTINATION ${INSTALL_PATH}/bin)
  1. 小型项目 / 原型:用接口库聚合依赖(如 mbedtls_deps),简化配置。
  2. 中大型项目:按功能拆分模块,用 PRIVATE 保证封装性,关键库显式安装确保版本可控。
  3. 生产环境:避免自动安装系统库(如 /usr/lib/libcurl.so),防止覆盖系统文件,核心库显式指定版本。
  1. 查看目标属性:验证依赖与路径配置

    get_target_property(link_libs novel_cli LINK_LIBRARIES)
    message(STATUS "novel_cli 链接的库: ${link_libs}")
    
    get_target_property(include_dirs novel_cli INTERFACE_INCLUDE_DIRECTORIES)
    message(STATUS "novel_cli 头文件路径: ${include_dirs}")
  2. 查看链接命令:确认库是否正确加入

    get_target_property(link_cmd novel_cli LINK_COMMAND)
    message(STATUS "链接命令: ${link_cmd}")
  3. 检查安装脚本:查看 CMake 生成的安装规则

    • 构建目录下 CMakeFiles/install_manifest.txt 记录所有待安装文件。

CMake 构建的核心是 “目标驱动”,通过精细化的目标定义、依赖传递控制和安装配置,实现工程的可维护性与可扩展性。关键原则:

  1. 区分目标与路径add 创建的库是目标(直接用名称),find 找到的库是路径(需用 ${})。
  2. 优先使用目标管理依赖:通过 INTERFACE_* 属性自动传递头文件和依赖,避免全局配置污染。
  3. 依赖传递遵循 “最小必要”PRIVATE 隐藏实现,PUBLIC 暴露接口,平衡封装性与易用性。
  4. 安装配置需分类处理:本地构建库用 install(TARGETS),外部导入库用 install(FILES),生产环境优先显式控制。

通过本文案例,可快速搭建从 “依赖管理→构建→安装” 的完整 CMake 工程流程,适配从原型开发到生产部署的全场景需求。