
1现代CMake核心基石目标-属性-API在进入库的学习前首先简历基于目标配置的现代CMake思维这也是CMake的核心三个概念。1目标Target目标是 CMake 的核心操作对象代表一个需要被生成的实体所有编译、链接、依赖配置都挂载在目标上。目标类型创建命令产物与说明可执行文件add_executable最终可运行的程序如main静态库add_library(... STATIC)Linux 下为.a、Windows 下为.lib编译时完整链接进二进制动态库add_library(... SHARED)Linux 下为.so、Windows 下为.dll程序运行时动态加载模块库add_library(... MODULE)插件形式的动态库通过dlopen运行时加载对象库add_library(... OBJECT)只编译生成.o目标文件不生成最终库用于复用编译结果接口库add_library(... INTERFACE)无实体文件仅传递头文件、编译选项、依赖等使用要求导入目标add_library(... IMPORTED)引用外部已编译好的库如第三方库别名目标add_library(... ALIAS)给现有目标起别名避免命名冲突2属性Property每个目标都有一组键值对形式的属性控制它的编译、链接、输出、安装等所有行为。属性的五大分类属性类别作用范围典型操作命令全局属性整个 CMake 运行周期set_property(GLOBAL ...)目录属性当前源码目录及子目录set_property(DIRECTORY ...)目标属性单个构建目标set_target_properties()源文件属性单个源码 / 资源文件set_source_files_properties()测试属性单个测试用例set_tests_properties()最关键属性作用域三关键字这是现代 CMake 最核心的规则决定了依赖和头文件路径是否向下游传播。关键字当前目标构建时生效向下游使用者传播典型场景PRIVATE✅ 是❌ 否仅内部实现使用的依赖不对外暴露PUBLIC✅ 是✅ 是公开 API 依赖头文件中用到的类型、接口INTERFACE❌ 否✅ 是纯接口库仅给下游传递使用要求3操作APICMake 提供了两套操作目标属性的方式专用语义命令推荐和通用读写接口。API 类别典型命令核心作用通用读写接口set_target_properties()get_target_property()读写任意目标属性最底层接口编译阶段target_include_directoriestarget_compile_optionstarget_compile_definitions配置头文件路径、编译选项、宏定义链接阶段target_link_librariestarget_link_directoriestarget_link_options配置依赖库、库搜索路径、链接选项输出与安装install(TARGETS ...)install(EXPORT ...)配置输出路径、安装规则、导出规则2静态库编译与项目内部引用我们通过一个数学运算库的完整例子学习如何制作静态库并在同项目的可执行文件中引用它。1工程目录结构my_math/ ├── CMakeLists.txt # 顶级配置文件 ├── app/ # 可执行程序模块 │ ├── CMakeLists.txt │ └── main.cpp └── my_lib/ # 静态库模块 ├── CMakeLists.txt ├── include/ │ └── math.h # 公开头文件 └── src/ ├── add.cpp └── sub.cpp2源代码与公共头文件1公开头文件my_lib/include/math.hint add(int a, int b); int sub(int a, int b);2库文件实现my_lib/src/add.cpp#include math.h int add(int a, int b) { return a b; }my_lib/src/sub.cpp同理实现减法函数。#include math.h int sub(int x,int y) { return x-y; }3可执行源代码app/main.cpp#include iostream #include math.h int main() { std::cout 3 4 add(3, 4) std::endl; std::cout 3 - 4 sub(3, 4) std::endl; return 0; }3三层CMake配置1顶层CMakeLists.txtcmake_minimum_required(VERSION 3.18) project(TestMyMath LANGUAGES CXX) # 添加子模块CMake 会递归执行子目录下的 CMakeLists.txt add_subdirectory(my_lib) add_subdirectory(app)2库模块配置my_lib/CMakeLists.txt# 1. 通配符收集所有源文件 file(GLOB SRC_LISTS src/*.cpp) # 2. 创建静态库目标 add_library(MyMath STATIC ${SRC_LISTS}) # 3. 设置头文件搜索路径 # PUBLIC自己编译时用下游使用者也会自动获得该路径 target_include_directories(MyMath PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include ) # 4. 修改静态库输出路径到构建目录下的 lib 文件夹 set_target_properties(MyMath PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib )3可执行模块配置app/CMakLists.txt# 1. 收集当前目录所有 cpp 文件 file(GLOB SRC_LISTS *.cpp) # 2. 创建可执行目标 add_executable(main ${SRC_LISTS}) # 3. 链接静态库 MyMath # PRIVATE只自己链接不继续向下传递 target_link_libraries(main PRIVATE MyMath) # 4. 设置可执行文件输出路径到构建目录下的 bin 文件夹 set_target_properties(main PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin )4核心命令解释1add_subdirectory作用将子目录加入构建树执行子目录中的CMakeLists.txt关键行为处理顺序立即暂停当前文件递归处理完子目录后再继续路径解析相对路径相对于当前 CMakeLists.txt 所在目录变量作用域子目录默认继承父目录变量但子目录修改变量不影响父目录如需向上传递用set(变量名 值 PARENT_SCOPE)内置变量变化进入子目录后CMAKE_CURRENT_SOURCE_DIR、CMAKE_CURRENT_BINARY_DIR、CMAKE_CURRENT_LIST_FILE、CMAKE_CURRENT_LIST_DIR都会切换为子目录路径与include的核心区别include不改变源码目录变量继承当前作用域add_subdirectory创建独立目录作用域切换源码路径。2add_libraryadd_library(库名 [STATIC|SHARED|MODULE] 源文件列表)不指定类型时默认创建静态库库名在项目内必须唯一Linux 下自动生成lib库名.a格式的静态库文件3target_include_directories作用给目标设置头文件搜索路径最终对应编译器的-I参数最佳实践永远使用该命令而不是全局的include_directories避免路径污染不相关的目标可选参数SYSTEM标记为系统头文件抑制第三方头文件产生的警告BEFORE将路径插到搜索列表最前面4target/_link_libraries作用设置目标的依赖库列表最终对应链接器的-l参数底层原理PRIVATE写入目标的LINK_LIBRARIES属性仅自己使用INTERFACE写入INTERFACE_LINK_LIBRARIES属性仅向下游传播PUBLIC同时写入两个属性自己用也向下传5file(GLOB)作用按通配符匹配文件将结果存入变量注意事项仅在 CMake 配置阶段执行一次新增源文件后需要重新执行cmake ..才能被识别递归匹配使用file(GLOB_RECURSE)遍历所有子目录6set_target_properties作用批量设置目标的任意属性配套读取命令get_target_property(变量 目标名 属性名)5构建和运行mkdir build cd build cmake .. cmake --build . # 运行生成的可执行文件 ./bin/main6原理静态库的定位流程CMake 能自动找到项目内部的静态库无需手动指定库路径完整流程分为四步目标注册执行add_library时CMake 在全局目标容器中注册该目标记录源文件、类型等信息属性存储所有输出路径、编译选项等属性都保存在目标对象上生成器推导配置结束进入生成阶段生成器根据属性推导出库的实际输出路径生成链接命令为依赖该库的目标生成链接指令时直接写入库的完整路径可以通过生成器表达式$TARGET_FILE:目标名获取目标的完整输出路径常用于自定义命令中# 构建完成后打印静态库的完整路径 add_custom_command( TARGET main POST_BUILD COMMAND ${CMAKE_COMMAND} -E echo $TARGET_FILE:MyMath COMMENT 获取静态库的输出路径 )3静态库安装发布与Config模式查找内部库只能在同项目内使用要让其他项目也能通过标准方式引用需要将库发布到系统路径并提供 CMake 配置文件。1发布标准库的核心四步安装库文件本体.a安装公开头文件安装导出目标文件记录目标属性与依赖安装包配置文件供find_package查找2发布版本完整配置修改my_lib/CMakeLists.txtfile(GLOB SRC_LISTS src/*.cpp) add_library(MyMath STATIC ${SRC_LISTS}) # 设置安装后的头文件路径仅对下游使用者生效 target_include_directories(MyMath INTERFACE $INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR} ) set_target_properties(MyMath PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib ) include(GNUInstallDirs) # 1. 安装库文件并关联到导出集 MyMathTargets install(TARGETS MyMath EXPORT MyMathTargets DESTINATION ${CMAKE_INSTALL_LIBDIR} ) # 2. 安装头文件到 include/math 目录下 install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/math FILES_MATCHING PATTERN *.h ) # 3. 导出目标到构建树仅当前构建目录可用 export(EXPORT MyMathTargets FILE ${CMAKE_CURRENT_BINARY_DIR}/MyMathTargets.cmake ) # 4. 安装导出目标到系统目录 install(EXPORT MyMathTargets FILE MyMathTargets.cmake NAMESPACE MyMath:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyMath ) # 5. 生成 Config 配置文件 include(CMakePackageConfigHelpers) configure_package_config_file( ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/MyMathConfig.cmake INSTALL_DESTINATION lib/cmake/MyMath ) # 6. 安装 Config 文件 install(FILES ${CMAKE_CURRENT_BINARY_DIR}/MyMathConfig.cmake DESTINATION lib/cmake/MyMath )3Config模版文件新建my_lib/Config.cmake.inPACKAGE_INIT include(${CMAKE_CURRENT_LIST_DIR}/MyMathTargets.cmake)4安装执行和产物分布mkdir build cd build cmake .. cmake --build . sudo cmake --install .安装后文件分布库文件/usr/local/lib/libMyMath.a头文件/usr/local/include/math/math.hCMake 配置/usr/local/lib/cmake/MyMath/目录下的.cmake文件5外部项目引用新建独立项目通过find_package即可使用cmake_minimum_required(VERSION 3.18) project(MyMathApp) # CONFIG 模式查找已安装的 MyMath 库 find_package(MyMath REQUIRED CONFIG) add_executable(main main.cpp) # 链接命名空间下的导入目标 target_link_libraries(main PRIVATE MyMath::MyMath)源码中头文件包含#include math/math.h6核心命令讲解1export和install(EXPORT)export(EXPORT)生成到构建目录仅当前构建树内可用install(EXPORT)生成并安装到系统供外部项目find_package加载NAMESPACE给导出目标加命名空间惯例为包名::避免命名冲突2configute_package_config_file作用根据模板生成标准的包配置文件自动处理路径相对化生成的文件符合find_packageConfig 模式的查找规范3write_basic_package_version_file作用生成版本校验文件支持find_package指定版本号兼容性选项AnyNewerVersion任何更新版本都兼容SameMajorVersion仅主版本号相同才兼容ExactVersion必须版本完全一致4动态库编译引用安装动态库共享库的 CMake 配置与静态库高度相似核心差异在于运行时加载机制和对应的属性配置。1动态库与静态库的核心差异维度静态库动态库链接时机编译链接时完整拷贝进二进制运行时才加载产物体积可执行文件体积大可执行文件体积小依赖外部库更新方式重新编译整个程序替换库文件即可不用重新编译运行依赖无额外依赖系统必须能找到库文件2项目内部引用动态库只需要将库类型从STATIC改为SHARED并补充动态库专属属性add_library(MyMath SHARED ${SRC_LISTS}) set_target_properties(MyMath PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib OUTPUT_NAME MyMath # 输出文件名 VERSION 1.2.3 # 完整库版本 SOVERSION 20 # API 主版本号 COMPILE_OPTIONS -fPIC # 生成位置无关代码动态库必须 )构建后会生成版本化的动态库文件libMyMath.so→ 开发用软链接libMyMath.so.20→ API 版本软链接libMyMath.so.1.2.3→ 实际库文件3原理拓展动态库运行时定位静态库在链接时就已嵌入二进制动态库需要在程序运行时找到库文件位置。CMake 通过RPATH机制解决构建时的运行问题构建阶段自动将构建目录下的库路径写入可执行文件的RUNPATH段运行时动态链接器读取RUNPATH找到动态库并加载验证方式# 查看 ELF 文件中的 RUNPATH 信息 readelf -d bin/main | grep PATH # 查看动态库依赖与解析结果 ldd bin/main4动态库的安装与查找安装流程与静态库完全一致仅目标类型不同安装后库文件存放在${CMAKE_INSTALL_LIBDIR}下。 外部项目查找使用方式也完全相同find_package会自动识别导入目标对应的是静态库还是动态库。5相对路径技巧 $ORIGIN$ORIGIN是 Linux 动态链接器的特殊变量代表可执行文件自身所在的目录是打包分发的核心技巧。# 设置安装后的 RPATH 为相对于可执行文件的 ../lib 目录 set_target_properties(main PROPERTIES INSTALL_RPATH $ORIGIN/../lib )这样无论程序解压到哪个位置都能自动找到同级 lib 目录下的动态库实现「解压即运行」。5第三方依赖管理find_packagefind_package是CMake管理第三方依赖的核心命令支持两种工作模式1两种查找模式总览模式查找对象适用场景Module 模式Find包名.cmake模块文件老旧库未提供 CMake 配置Config 模式包名Config.cmake配置文件现代库作者自带 CMake 配置默认查找顺序先尝试 Config 模式失败再回退到 Module 模式可通过MODULE或CONFIG参数强制指定。2Module模式详解查找模块文件先搜索CMAKE_MODULE_PATH指定的目录再搜索 CMake 内置模块目录执行模块逻辑内部通常调用find_path找头文件、find_library找库文件、检查版本输出结果变量设置包名_FOUND、包名_INCLUDE_DIRS、包名_LIBRARIES通常还会创建导入目标3Config模式详解查找配置文件搜索CMAKE_PREFIX_PATH、系统标准路径如/usr/local/lib/cmake/执行配置文件库作者预定义好导入目标、依赖关系、头文件路径提供命名空间目标如JsonCpp::JsonCpp直接链接即可自动传递所有使用要求4实战查找JsonCpp库1Module模式先编写自定义模块文件cmake/FindJsonCpp.cmake# 查找头文件 find_path(JsonCpp_INCLUDE_DIR NAMES json/json.h PATHS /usr/include /usr/local/include PATH_SUFFIXES jsoncpp ) # 查找库文件 find_library(JsonCpp_LIBRARY NAMES jsoncpp libjsoncpp PATHS /usr/lib /usr/local/lib ) # 设置结果变量并创建导入目标 if(JsonCpp_INCLUDE_DIR AND JsonCpp_LIBRARY) set(JsonCpp_FOUND TRUE) set(JsonCpp_INCLUDE_DIRS ${JsonCpp_INCLUDE_DIR}) set(JsonCpp_LIBRARIES ${JsonCpp_LIBRARY}) if(NOT TARGET JsonCpp::JsonCpp) add_library(JsonCpp::JsonCpp UNKNOWN IMPORTED) set_target_properties(JsonCpp::JsonCpp PROPERTIES IMPORTED_LOCATION ${JsonCpp_LIBRARY} INTERFACE_INCLUDE_DIRECTORIES ${JsonCpp_INCLUDE_DIRS} ) endif() else() set(JsonCpp_FOUND FALSE) endif() mark_as_advanced(JsonCpp_INCLUDE_DIR JsonCpp_LIBRARY)项目中使用# 添加自定义模块目录 list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) # MODULE 模式查找 find_package(JsonCpp REQUIRED MODULE) add_executable(myapp main.cpp) target_link_libraries(myapp PRIVATE JsonCpp::JsonCpp)2Config模式安装了带 CMake 配置的 JsonCpp 后直接一行即可find_package(jsoncpp CONFIG REQUIRED) target_link_libraries(main PRIVATE JsonCpp::JsonCpp)5拓展pkg-config方式很多系统库提供 pkg-config 配置CMake 可以通过 PkgConfig 模块调用find_package(PkgConfig REQUIRED) pkg_check_modules(JSONCPP REQUIRED IMPORTED_TARGET jsoncpp) add_executable(main main.cpp) target_link_libraries(main PRIVATE PkgConfig::JSONCPP)6集成外部工具CMake可以调用外部命令生成代码并自动集成到构建流程典型场景是Protocol Buffers的.proto文件编译1实现思路查找系统中的 Protobuf 库与protoc编译器遍历所有.proto文件为每个文件生成对应的.pb.cc和.pb.h将生成的代码编译为静态库业务目标链接该静态库即可使用2完整的CMake配置cmake_minimum_required(VERSION 3.18) project(ProtocExample LANGUAGES CXX ) #设置C标准 set(CMAKE_CXX_STANDARD 11) #1查找protobuf库要求版本3.0 find_package(Protobuf 3.0 REQUIRED) #2收集proto项目描述文件 file(GLOB PROTO_FILES ${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto) #3创建protoc的生成文件的目录 set(PB_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/pb) file(MAKE_DIRECTORY ${PB_OUT_DIR}) #4初始化生成的源代码文件集合,用于生成我们的静态库·,用main使用 set(GEN_SRCS ) # *.pb.cc set(GEN_HEADS )# *.pb.h #5为每一个proto文件生成对应的.cc和.h文件 foreach(PROTO ${PROTO_FILES}) #5.1获取不带扩展名的文件名 get_filename_component(BASE_NAME ${PROTO} NAME_WE) #person #5.2添加生成的c文件到源代码集合 list(APPEND GEN_SRCS ${PB_OUT_DIR}/${BASE_NAME}.pb.cc) list(APPEND GEN_HEADS ${PB_OUT_DIR}/${BASE_NAME}.pb.h) #5.3为每一个proto文件生成C文件 add_custom_command( OUTPUT ${PB_OUT_DIR}/${BASE_NAME}.pb.cc ${PB_OUT_DIR}/${BASE_NAME}.pb.h COMMAND protoc ARGS --cpp_out${PB_OUT_DIR} -I ${CMAKE_CURRENT_SOURCE_DIR}/proto ${PROTO} DEPENDS ${PROTO} COMMENT 从*.proto生成对应的C代码 VERBATIM ) endforeach() #6添加触发生成pb文件的目标 add_custom_target(generate_protobuf DEPENDS ${GEN_SRCS} ${GEN_HEADS}) #7吧protoc生成的C文件编译成静态库 add_library(MyProto STATIC ${GEN_SRCS}) add_dependencies(MyProto generate_protobuf) #main.cpp 需要 #include person.pb.h这里要暴露生成目录为头文件搜索路径 target_include_directories(MyProto PUBLIC ${PB_OUT_DIR} ) target_link_libraries(MyProto PUBLIC protobuf::libprotobuf) #8添加可执行程序 add_executable(main main.cpp) #9添加依赖关系 target_link_libraries(main PRIVATE MyProto)3. 核心命令说明1add_custom_command作用定义自定义构建规则关键参数OUTPUT该命令生成的文件COMMAND要执行的外部命令DEPENDS依赖文件依赖变化时重新执行命令POST_BUILD在指定目标构建完成后执行2add_custom_target作用创建一个虚拟目标用于触发自定义命令通过add_dependencies让其他目标依赖它保证构建顺序正确7CTest集成测试框架1基础使用cmake_minimum_required(VERSION 3.18) project(TestMyMath LANGUAGES CXX ) #1开启测试功能 include(CTest) #2添加测试执行文件 add_executable(main main.cpp) #3查找MyMAth库 find_package(MyMath CONFIG REQUIRED) #4链接MyMath库 target_link_libraries(main PRIVATE MyMath::MyMath) #5添加测试Case到CTest add_test( NAME TestMyMath COMMAND main )2运行测试# 方式1直接调用 ctest ctest # 方式2通过 make 调用 make testCTest 会自动执行所有注册的测试用例输出通过率、耗时支持并行执行、按名称筛选测试等高级功能。3最佳实践将测试作为构建流程的一环测试全部通过后再执行安装和发布保证发布版本的质量。8CPack打包分发工具CPack 是 CMake 自带的打包工具可以将项目一键打包成 TGZ、DEB、RPM、ZIP 等多种分发格式。1CPack核心功能自动收集基于install()规则自动收集所有要打包的文件多格式支持跨平台生成各种安装包格式元数据管理自动包含版本号、项目名称等信息2基础配置在顶级 CMakeLists.txt 末尾添加include(CPack) set(CPACK_PACKAGE_NAME MyMathApp) set(CPACK_PACKAGE_VERSION 1.0.0) set(CPACK_GENERATOR TGZ) # 指定打包格式为 tar.gz同时给可执行文件设置相对路径 RPATH保证解压即运行set_target_properties(main PROPERTIES INSTALL_RPATH $ORIGIN/../lib )3打包与验证# 执行打包 cpack # 或 make package生成的压缩包解压后的目录结构MyMathApp-1.0.0-Linux/ ├── bin/ │ └── main └── lib/ └── libMyMath.so进入bin目录直接运行./main即可正常执行动态库会通过$ORIGIN/../lib自动定位。