【学习记录】CMake 多模块项目构建指南从库编译到可执行文件集成在 C 项目开发中CMake 是管理编译过程的核心工具。本文从一个实际的多模块项目出发详细解析了根目录配置、静态/动态库构建、可执行文件链接三个层次的 CMakeLists.txt 设计。通过逐段拆解展示如何将源码组织为可复用的库并让多个 Demo 程序共享同一份编译产物。文章覆盖了变量传递、输出目录统一、构建类型适配、导入目标等核心概念并给出了常见问题的解决方案适合作为中大型 C 项目 CMake 配置的参考模板。 目录项目整体结构最外层 CMakeLists.txt项目入口与子目录组织DNI_library/CMakeLists.txt库的构建与输出3.1 基础配置与策略3.2 源码收集与库目标创建3.3 头文件路径与编译定义3.4 输出目录的统一管理Demo1/CMakeLists.txt可执行文件与库的链接4.1 导入已构建的库目标4.2 可执行文件的生成与链接总结一、项目整体结构项目采用分层组织方式根目录下包含一个核心库目录DNI_library和多个 Demo 目录Demo1至Demo4。编译产物统一输出到根目录下的bin文件夹。project_root/ ├── CMakeLists.txt # 最外层 ├── DNI_library/ │ ├── CMakeLists.txt # 库构建配置 │ ├── library/ │ │ ├── include/ # 公开头文件 │ │ └── src/ │ │ └── include/ # 内部头文件 │ └── src/ # 库源码 ├── Demo1/ │ ├── CMakeLists.txt # 可执行文件配置 │ └── main.cpp ├── Demo2/ │ └── ... └── bin/ # 统一输出目录.exe, .dll, .lib, .pdb二、最外层 CMakeLists.txt项目入口与子目录组织cmake_minimum_required(VERSION 3.15) project(dni_polaris_lyra LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) add_subdirectory(DNI_library) add_subdirectory(Demo1) add_subdirectory(Demo2) add_subdirectory(Demo3) add_subdirectory(Demo4)解析指令作用cmake_minimum_required(VERSION 3.15)指定 CMake 最低版本确保使用较新的语法特性。project(... LANGUAGES C CXX)声明项目名称和使用的语言C 和 C。set(CMAKE_CXX_STANDARD 20)强制使用 C20 标准。set(CMAKE_CXX_EXTENSIONS OFF)禁用编译器特有的扩展如 GCC 的-stdgnu20保证代码跨平台兼容。add_subdirectory(...)递归进入子目录执行其中的 CMakeLists.txt。这是构建系统的“入口链”。设计要点标准统一在顶层强制 C20 标准所有子目录继承该设置。模块化每个 Demo 作为一个独立子项目互不干扰。构建顺序DNI_library必须先于 Demo 子目录被add_subdirectory因为 Demo 依赖于库的构建结果。三、DNI_library/CMakeLists.txt库的构建与输出3.1 基础配置与策略cmake_minimum_required(VERSION 3.15) project(DNI_Library VERSION 1.0.0 LANGUAGES CXX) set(PROJECT_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..) set(DNI_OUTPUT_DIR ${PROJECT_ROOT}/bin) if(POLICY CMP0141) cmake_policy(SET CMP0141 NEW) set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT $IF:$AND:$C_COMPILER_ID:MSVC,$CXX_COMPILER_ID:MSVC,$$CONFIG:Debug,RelWithDebInfo:EditAndContinue,$$CONFIG:Debug,RelWithDebInfo:ProgramDatabase) endif() option(BUILD_SHARED_LIBS Build shared library ON) if(BUILD_SHARED_LIBS) set(LIB_TYPE SHARED) else() set(LIB_TYPE STATIC) endif()解析配置项含义project(... VERSION 1.0.0)为库设置版本号可用于生成版本头文件或导出信息。PROJECT_ROOT通过CMAKE_CURRENT_SOURCE_DIR/..获取根目录的绝对路径。DNI_OUTPUT_DIR所有编译产物.exe, .dll, .lib, .pdb统一输出到根目录下的bin。CMP0141MSVC 调试信息格式策略用于控制.pdb文件的生成方式。option(BUILD_SHARED_LIBS ...)提供开关允许用户选择构建动态库.dll或静态库.lib。关于CMAKE_MSVC_DEBUG_INFORMATION_FORMAT该策略控制 MSVC 编译器生成的调试信息格式。EditAndContinue支持“编辑并继续”调试功能仅 Debug/RelWithDebInfo 配置而ProgramDatabase生成独立的.pdb文件。此处通过生成器表达式$...根据编译器和构建配置动态选择。可以在开发阶段用 ON动态库方便调试时快速替换在最终交付客户时切到 OFF静态库只给客户 4 个独立的 .exe 文件省去部署 DLL 的麻烦。3.2 源码收集与库目标创建file(GLOB SOURCESLIB ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/library/src/*.cpp ) add_library(DNIlib ${LIB_TYPE} ${SOURCESLIB})解析file(GLOB ...)收集src/和library/src/下的所有.cpp文件。⚠️注意GLOB不会在新增/删除源文件时自动更新 CMake 缓存若频繁增删源码建议显式列出源文件或每次修改后重新运行 CMake。add_library(DNIlib ${LIB_TYPE} ${SOURCESLIB})根据LIB_TYPE变量创建共享库或静态库。LIB_TYPE由BUILD_SHARED_LIBS选项控制。3.3 头文件路径与编译定义target_include_directories(DNIlib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/library/include ${CMAKE_CURRENT_SOURCE_DIR}/library/src/include ${CMAKE_CURRENT_SOURCE_DIR} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ) target_compile_definitions(DNIlib PRIVATE CAPICOMMON_EXPORTS)解析关键字作用PUBLIC头文件路径会传递给链接该库的目标Demo 程序且会被导出到库的接口中。PRIVATE头文件路径仅对库本身可见不传递给链接者。CAPICOMMON_EXPORTS通常用于 Windows 上导出符号的宏定义告诉编译器当前正在构建 DLL 而非使用 DLL。路径设计说明library/include/对外公开的 API 头文件。library/src/include/库内部实现使用但也被设为PUBLIC意味着 Demo 也可以包含它们。在某些场景下这可能是为了方便调试或测试但理想情况下内部头文件应设为PRIVATE。src/仅库内部使用设置为PRIVATE。3.4 输出目录的统一管理set_target_properties(DNIlib PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${DNI_OUTPUT_DIR} LIBRARY_OUTPUT_DIRECTORY ${DNI_OUTPUT_DIR} RUNTIME_OUTPUT_DIRECTORY ${DNI_OUTPUT_DIR} PDB_OUTPUT_DIRECTORY ${DNI_OUTPUT_DIR} ) foreach(CONFIG_TYPE DEBUG RELEASE RELWITHDEBINFO MINSIZEREL) set_target_properties(DNIlib PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_${CONFIG_TYPE} ${DNI_OUTPUT_DIR} LIBRARY_OUTPUT_DIRECTORY_${CONFIG_TYPE} ${DNI_OUTPUT_DIR} RUNTIME_OUTPUT_DIRECTORY_${CONFIG_TYPE} ${DNI_OUTPUT_DIR} PDB_OUTPUT_DIRECTORY_${CONFIG_TYPE} ${DNI_OUTPUT_DIR} ) endforeach()解析属性对应文件类型ARCHIVE_OUTPUT_DIRECTORY静态库.lib或.aLIBRARY_OUTPUT_DIRECTORY动态库.so非 WindowsRUNTIME_OUTPUT_DIRECTORY可执行文件.exe和动态库.dllWindowsPDB_OUTPUT_DIRECTORY程序数据库.pdbWindows 调试符号分别设置了默认输出目录set_target_properties不带_${CONFIG_TYPE}和针对每种构建配置的专用输出目录带后缀的_DEBUG、_RELEASE等。统一输出到bin/目录方便后续链接和部署。四、Demo1/CMakeLists.txt可执行文件与库的链接4.1 导入已构建的库目标cmake_minimum_required(VERSION 3.15) project(Demo01_ToolPoseCollector LANGUAGES CXX) get_filename_component(PROJECT_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/.. ABSOLUTE) set(DNI_OUTPUT_DIR ${PROJECT_ROOT}/bin) set(DNI_LIBRARY_ROOT ${PROJECT_ROOT}/DNI_library) if(NOT TARGET DNIlib) add_library(DNIlib SHARED IMPORTED GLOBAL) set_target_properties(DNIlib PROPERTIES IMPORTED_IMPLIB ${DNI_OUTPUT_DIR}/DNIlib.lib IMPORTED_LOCATION ${DNI_OUTPUT_DIR}/DNIlib.dll INTERFACE_INCLUDE_DIRECTORIES ${DNI_LIBRARY_ROOT}/library/include;${DNI_LIBRARY_ROOT}/library/src/include;${DNI_LIBRARY_ROOT} ) endif()解析add_library(DNIlib SHARED IMPORTED GLOBAL)声明一个导入目标Imported Target告诉 CMake 这个库已经在外部构建好了不需要在当前目录下重新编译。IMPORTED_IMPLIB指向导入库文件.libWindows 下用于链接。IMPORTED_LOCATION指向动态库文件.dll运行时需要。INTERFACE_INCLUDE_DIRECTORIES指定使用该库时需要包含的头文件路径。NOT TARGET DNIlib检查确保不会重复导入防止因add_subdirectory顺序导致目标已存在。4.2 可执行文件的生成与链接add_executable(Demo01_ToolPoseCollector main.cpp) target_link_libraries(Demo01_ToolPoseCollector PRIVATE DNIlib) target_include_directories(Demo01_ToolPoseCollector PRIVATE ${DNI_LIBRARY_ROOT}/library/include ${DNI_LIBRARY_ROOT}/library/src/include ${DNI_LIBRARY_ROOT} ) set_target_properties(Demo01_ToolPoseCollector PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${DNI_OUTPUT_DIR} PDB_OUTPUT_DIRECTORY ${DNI_OUTPUT_DIR} ) foreach(CONFIG_TYPE DEBUG RELEASE RELWITHDEBINFO MINSIZEREL) set_target_properties(Demo01_ToolPoseCollector PROPERTIES RUNTIME_OUTPUT_DIRECTORY_${CONFIG_TYPE} ${DNI_OUTPUT_DIR} PDB_OUTPUT_DIRECTORY_${CONFIG_TYPE} ${DNI_OUTPUT_DIR} ) endforeach()解析target_link_libraries(... PRIVATE DNIlib)将可执行文件与导入的库链接。target_include_directories虽然本例中通过IMPORTED目标的INTERFACE_INCLUDE_DIRECTORIES已经传递了头文件路径但显式再设置一次可以避免依赖顺序问题或用于非PUBLIC路径。输出目录同样统一到bin/。五、常见问题与调试技巧问题原因与解决方案链接时找不到DNIlib.lib检查DNI_OUTPUT_DIR是否指向正确的生成目录。确保库已先编译。运行时找不到DNIlib.dll将bin/添加到系统PATH环境变量或将.dll复制到可执行文件所在目录。头文件路径冲突检查INTERFACE_INCLUDE_DIRECTORIES是否包含重复或错误的路径。CMAKE_MSVC_DEBUG_INFORMATION_FORMAT报错确保 CMake 版本 ≥ 3.15且编译环境为 MSVC。若使用其他编译器该策略会被忽略。源文件变更后构建未更新因file(GLOB)不会自动追踪新增文件需手动重新运行 CMake 配置。 总结本文从一个真实的多模块 C 项目出发解析了 CMake 在三个层级上的配置逻辑层级职责根 CMakeLists统一标准组织子目录决定构建顺序。库 CMakeLists收集源码创建共享/静态库管理头文件可见性统一输出目录。Demo CMakeLists导入预编译的库创建可执行文件链接依赖。核心设计思想目录统一所有构建产物.exe,.dll,.lib,.pdb输出到同一个bin/目录简化部署和链接。模块解耦库和 Demo 独立构建Demo 通过导入目标Imported Target引用库无需重新编译库源码。跨配置适配为 Debug/Release/RelWithDebInfo 分别设置输出路径保证多配置构建互不干扰。策略兼容通过cmake_policy和生成器表达式适配 MSVC 调试格式提升 Windows 下的开发体验。这套 CMake 配置结构清晰、易于扩展可直接作为中大型 C 项目的构建模板。后续可进一步集成第三方依赖管理如find_package、vcpkg或添加单元测试子目录。原文最外层CMakeLists.txtcmake_minimum_required(VERSION3.15)project(dni_polaris_lyra LANGUAGES C CXX)set(CMAKE_CXX_STANDARD20)set(CMAKE_CXX_STANDARD_REQUIRED ON)set(CMAKE_CXX_EXTENSIONS OFF)add_subdirectory(DNI_library)add_subdirectory(Demo1)add_subdirectory(Demo2)add_subdirectory(Demo3)add_subdirectory(Demo4)生成库CMakeLists.txtcmake_minimum_required(VERSION3.15)project(DNI_Library VERSION1.0.0LANGUAGES CXX)set(PROJECT_ROOT${CMAKE_CURRENT_SOURCE_DIR}/..)set(DNI_OUTPUT_DIR${PROJECT_ROOT}/bin)if(POLICY CMP0141)cmake_policy(SET CMP0141 NEW)set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT$IF:$AND:$C_COMPILER_ID:MSVC,$CXX_COMPILER_ID:MSVC,$$CONFIG:Debug,RelWithDebInfo:EditAndContinue,$$CONFIG:Debug,RelWithDebInfo:ProgramDatabase)endif()option(BUILD_SHARED_LIBSBuild shared libraryON)if(BUILD_SHARED_LIBS)set(LIB_TYPE SHARED)else()set(LIB_TYPE STATIC)endif()file(GLOB SOURCESLIB${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp${CMAKE_CURRENT_SOURCE_DIR}/library/src/*.cpp)add_library(DNIlib ${LIB_TYPE}${SOURCESLIB})target_include_directories(DNIlib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/library/include ${CMAKE_CURRENT_SOURCE_DIR}/library/src/include ${CMAKE_CURRENT_SOURCE_DIR}PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)target_compile_definitions(DNIlib PRIVATE CAPICOMMON_EXPORTS)# 如果有第三方库在这里添加#target_link_libraries(DNIlib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/library/lib/xxx.lib)set_target_properties(DNIlib PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${DNI_OUTPUT_DIR}LIBRARY_OUTPUT_DIRECTORY ${DNI_OUTPUT_DIR}RUNTIME_OUTPUT_DIRECTORY ${DNI_OUTPUT_DIR}PDB_OUTPUT_DIRECTORY ${DNI_OUTPUT_DIR})foreach(CONFIG_TYPE DEBUG RELEASE RELWITHDEBINFO MINSIZEREL)set_target_properties(DNIlib PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_${CONFIG_TYPE}${DNI_OUTPUT_DIR}LIBRARY_OUTPUT_DIRECTORY_${CONFIG_TYPE}${DNI_OUTPUT_DIR}RUNTIME_OUTPUT_DIRECTORY_${CONFIG_TYPE}${DNI_OUTPUT_DIR}PDB_OUTPUT_DIRECTORY_${CONFIG_TYPE}${DNI_OUTPUT_DIR})endforeach()if(CMAKE_VERSION VERSION_GREATER_EQUAL3.12)set_property(TARGET DNIlib PROPERTY CXX_STANDARD20)endif()message(STATUSDNIlib sources: ${SOURCESLIB})生成.exe CMakeLists.txtcmake_minimum_required(VERSION3.15)project(Demo01_ToolPoseCollector LANGUAGES CXX)get_filename_component(PROJECT_ROOT${CMAKE_CURRENT_SOURCE_DIR}/..ABSOLUTE)set(DNI_OUTPUT_DIR${PROJECT_ROOT}/bin)set(DNI_LIBRARY_ROOT${PROJECT_ROOT}/DNI_library)if(NOT TARGET DNIlib)add_library(DNIlib SHARED IMPORTED GLOBAL)set_target_properties(DNIlib PROPERTIES IMPORTED_IMPLIB${DNI_OUTPUT_DIR}/DNIlib.libIMPORTED_LOCATION${DNI_OUTPUT_DIR}/DNIlib.dllINTERFACE_INCLUDE_DIRECTORIES${DNI_LIBRARY_ROOT}/library/include;${DNI_LIBRARY_ROOT}/library/src/include;${DNI_LIBRARY_ROOT})endif()add_executable(Demo01_ToolPoseCollector main.cpp)target_link_libraries(Demo01_ToolPoseCollector PRIVATE DNIlib)target_include_directories(Demo01_ToolPoseCollector PRIVATE${DNI_LIBRARY_ROOT}/library/include${DNI_LIBRARY_ROOT}/library/src/include${DNI_LIBRARY_ROOT})set_target_properties(Demo01_ToolPoseCollector PROPERTIES RUNTIME_OUTPUT_DIRECTORY${DNI_OUTPUT_DIR}PDB_OUTPUT_DIRECTORY${DNI_OUTPUT_DIR})foreach(CONFIG_TYPE DEBUG RELEASE RELWITHDEBINFO MINSIZEREL)set_target_properties(Demo01_ToolPoseCollector PROPERTIES RUNTIME_OUTPUT_DIRECTORY_${CONFIG_TYPE}${DNI_OUTPUT_DIR}PDB_OUTPUT_DIRECTORY_${CONFIG_TYPE}${DNI_OUTPUT_DIR})endforeach()