往期本博主的 C++ 精讲优质博文可通过这篇导航进行查找:
Lemo 的C++精华博文导航:进阶、精讲、设计模式文章全收录
前言
在当今的软件开发环境中,随着 C++ 项目的不断扩大和变得越来越复杂,有效地管理这些项目成为了一个重要的挑战。 CMake 作为一个跨平台的构建系统,提供了强大的工具来帮助开发者组织和管理大型 C++ 项目。
在本文中,我们将探讨为什么使用 CMake 进行 C++ 大型项目管理以及如何使用通用 CMake 框架进行项目工程的搭建。
文章目录
- 前言
- 为什么用 CMake 进行 C++ 大型项目管理
- 通用 CMake 进行 C++ 项目工程搭建的方法
- 一个栗子
- 总结
为什么用 CMake 进行 C++ 大型项目管理
CMake 的优势在于它的平台无关性以及能够管理复杂项目的能力。使用 CMake,开发者可以编写一次构建脚本(CMakeLists.txt 文件),然后在任何支持 CMake 的平台上生成项目。这不仅简化了构建过程,还确保了项目的可移植性。
此外,CMake 支持复杂的项目结构,包括对多个目标和库的管理,以及对依赖关系的精细控制。 CMake 能自动处理源代码文件与目标之间的依赖关系,这对于大型项目而言尤为重要,因为它们往往包含成百上千个文件和复杂的依赖链。
CMake还提供了丰富的功能,以支持现代C++项目的需要,如测试(CTest)、打包(CPack)等,这使得它成为大型项目的理想选择。
通用 CMake 进行 C++ 项目工程搭建的方法
在使用 CMake 构建大型 C++ 项目时,首先需要明确的是,良好的项目结构对项目的可维护性和可扩展性至关重要。
以下是一些常用的实践方法,旨在帮助开发者有效地使用CMake构建和管理项目。
分层项目结构: 为了有效管理项目的不同部分,推荐将项目分成多个逻辑上的层或模块,如UI层、业务逻辑层、数据访问层等。每个层都应该有自己的CMakeLists.txt文件,定义了该层所需的源文件、库依赖等。
使用全局和局部CMakeLists.txt文件: 在项目的根目录下有一个全局CMakeLists.txt文件,它不仅包含了项目的基本信息(如项目名、版本等),还负责将各个子项目/模块组织起来。而每个子目录中的CMakeLists.txt文件则负责处理该目录中的具体构建规则。这种层次化的方法有助于分离不同部分的构建逻辑,便于管理和理解。
找寻和使用第三方库: 当项目需要第三方库时,CMake的 find_package()
命令可以自动检测并配置项目中使用的依赖库。这简化了配置过程,并增加了项目的移植性。
创建可配置的构建选项: 利用CMake的 option()
命令,可以定义可在构建时配置的选项。这对于需要支持多种构建配置的大型项目特别有用。
编写可复用的CMake模块: 对于一些常用的构建逻辑,可以编写自定义的CMake模块来实现代码的复用。这些模块可以被项目中的不同部分所共享,减少了重复代码。
我们通过一个例子,来看这些实践方法是如何作用的。
一个栗子
假设我们有如下C++工程结构:
Demo|| ---- modules| | ---- CMakeLists.txt| | ---- Module1| | | ----inc| | | ----src| | | ----CMakeLists.txt| || | ---- Module2| | ----inc| | ----src| | ----CMakeLists.txt|| ---- thirdparty| | ----include| | ----lib| | ----bin|| ---- utils| | ---- SettingUtils.cmake|| ---- CMakeLists.txt
即,我们有一个叫 Demo 的工程,里面有一个模块集 Modules
两个子模块:Module1
和 Module2
;还依赖第三方件:thirdparty
;自有一个辅助文件夹 utils
。
我们来为这个项目设计 CMake 框架:
在 Demo 项目根目录的 CMakeLists.txt
我们通常设置一些全局信息:
cmake_minimum_required(VERSION 3.1)set(PROJECT_NAME MySolution)
project(${PROJECT_NAME})set_property(GLOBAL PROPERTY USE_FOLDERS ON)if (NOT CMAKE_BUILD_TYPE)set(CMAKE_BUILD_TYPE "Release")
endif()include(utils/SettingUtils.cmake)string(REPLACE "\\" "/" THIRDPARTY_ROOT ${THIRDPARTY_ROOT})if (NOT EXISTS ${THIRDPARTY_ROOT})message(FATAL_ERROR "Please specify the THIRDPARTY_ROOT")
endif()
message("Based on WUKONG SecDev package at ${THIRDPARTY_ROOT}")DEFAULT_CONFIGURATION_SETTINGS()include_directories("${THIRDPARTY_ROOT}/include/inc1""${THIRDPARTY_ROOT}/include/inc2")ADD_SUBDIRECTORY(modules)
在此,我们设置了该 CMake 工程的最低 cmake 版本,设置了生成解决方案的名字,依赖第三方库的项,以及一些编译链接的宏设置。
我们接下来看里面怎么写:
可复用的 CMake 代码 SettingUtils.cmake
MACRO(DEFAULT_CONFIGURATION_SETTINGS)set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "Configurations" FORCE)if (NOT CMAKE_BUILD_TYPE)set(CMAKE_BUILD_TYPE "Release")endif()
ENDMACRO(DEFAULT_CONFIGURATION_SETTINGS)MACRO(SOLUTION_DEFINITIONS_DEFAULT_SETTINGS)add_definitions(-DWIN32-DUNICODE-D_UNICODE)
ENDMACRO(SOLUTION_DEFINITIONS_DEFAULT_SETTINGS)MACRO(SOLUTION_COMPILE_OPTIONS_DEFAULT_SETTRINGS)set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_RELEASE}")set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_RELEASE}")set(CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")if (CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Debug"))string(REPLACE "/O2" "/Od" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}")string(REPLACE "/O2" "/Od" CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}")set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /Zi")set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /Zi")add_link_options(/DEBUG)elseif (CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Release"))add_definitions(-DNDEBUG)# 编译器优化add_compile_options(/Ob1)add_compile_options(/Oi)add_compile_options(/Ot)add_compile_options(/GF)add_compile_options(/Gy)endif()add_compile_options(/W3)add_compile_options(/utf-8)add_compile_options(/std:c++14)add_compile_options(/wd4819)add_compile_options(/wd4275)add_compile_options(/wd4251)add_link_options(/SUBSYSTEM:WINDOWS)
ENDMACRO(SOLUTION_COMPILE_OPTIONS_DEFAULT_SETTRINGS)MACRO(IDE_DEBUG_ENVIRONMENT_DEFAULT_SETTING)IF(MSVC)SET_PROPERTY(DIRECTORY ${CMAKE_BINARY_DIR} PROPERTY VS_STARTUP_PROJECT ${startup_project})SET_TARGET_PROPERTIES(${startup_project} PROPERTIES VS_DEBUGGER_COMMAND ${THIRDPARTY_ROOT}/bin/Main.exeVS_DEBUGGER_WORKING_DIRECTORY ${THIRDPARTY_ROOT}/bin)ENDIF()
ENDMACRO(IDE_DEBUG_ENVIRONMENT_DEFAULT_SETTING)MACRO(IDE_INSTALL_ENVIRONMENT_DEFAULT_SETTING)install(DIRECTORY ${PROJECT_BINARY_DIR}/${CMAKE_BUILD_TYPE}/DESTINATION ${THIRDPARTY_ROOT}/bin/FILES_MATCHING PATTERN "*.dll")
ENDMACRO(IDE_INSTALL_ENVIRONMENT_DEFAULT_SETTING)MACRO(IDE_INSTALL_LIB_PDB_SETTING)add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_BINARY_DIR}/${CMAKE_BUILD_TYPE}/${PROJECT_NAME}.lib${THIRDPARTY_ROOT}/lib/${PROJECT_NAME}.lib)IF(CMAKE_BUILD_TYPE STREQUAL "Debug")add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_BINARY_DIR}/${CMAKE_BUILD_TYPE}/${PROJECT_NAME}.pdb${THIRDPARTY_ROOT}/pdb/${PROJECT_NAME}.pdb)ENDIF()
ENDMACRO(IDE_INSTALL_LIB_PDB_SETTING)MACRO(IDE_INSTALL_DEFAULT_DLL_SETTING)add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_BINARY_DIR}/${CMAKE_BUILD_TYPE}/${PROJECT_NAME}.dll${THIRDPARTY_ROOT}/bin/${PROJECT_NAME}.dll)
ENDMACRO(IDE_INSTALL_DEFAULT_DLL_SETTING)MACRO(IDE_INSTALL_PLUGINS_DLL_SETTING)add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_BINARY_DIR}/${CMAKE_BUILD_TYPE}/${PROJECT_NAME}.dll${THIRDPARTY_ROOT}/bin/plugins/designer/${PROJECT_NAME}.dll)
ENDMACRO(IDE_INSTALL_PLUGINS_DLL_SETTING)MACRO(IDE_INSTALL_EXE_SETTING)add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_BINARY_DIR}/${CMAKE_BUILD_TYPE}/${PROJECT_NAME}.exe${THIRDPARTY_ROOT}/bin/${PROJECT_NAME}.exe)IF(CMAKE_BUILD_TYPE STREQUAL "Debug")add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_BINARY_DIR}/${CMAKE_BUILD_TYPE}/${PROJECT_NAME}.pdb${THIRDPARTY_ROOT}/pdb/${PROJECT_NAME}.pdb)ENDIF()
ENDMACRO(IDE_INSTALL_EXE_SETTING)MACRO(RESOURCE_AND_CONFIG_DEFAULT_SETTING)install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../tools/xxx.batDESTINATION ${THIRDPARTY_ROOT}/bin/)install(CODE "execute_process(COMMAND cmd /c ${THIRDPARTY_ROOT}/bin/xxx.bat)")
ENDMACRO(RESOURCE_AND_CONFIG_DEFAULT_SETTING)
我们来看下这些宏都是什么意思:
DEFAULT_CONFIGURATION_SETTINGS
设置默认的编译类别是哪些SOLUTION_DEFINITIONS_DEFAULT_SETTINGS
设置默认的解决方案相关的宏定义SOLUTION_COMPILE_OPTIONS_DEFAULT_SETTRINGS
进行默认的编译、链接选项设置IDE_DEBUG_ENVIRONMENT_DEFAULT_SETTING
设置解决方案启动项IDE_INSTALL_ENVIRONMENT_DEFAULT_SETTING
默认 install 时的设置IDE_INSTALL_LIB_PDB_SETTING
lib 和 pdb 文件的拷贝IDE_INSTALL_DEFAULT_DLL_SETTING
dll 文件的拷贝IDE_INSTALL_PLUGINS_DLL_SETTING
插件 dll 文件的拷贝IDE_INSTALL_EXE_SETTING
exe 文件的拷贝RESOURCE_AND_CONFIG_DEFAULT_SETTING
资源和配置文件的设置
我们接着往 modules 文件夹里面看,看这里面的 CMakeLists.txt 文件该如何编写:
ADD_SUBDIRECTORY(Module1)
ADD_SUBDIRECTORY(Module2)
这一层的 CMakeLists.txt 直接指向内层的文件夹就行。
对于里面的 Module1
、 Module2
,都是到了具体的模块业务,只用编写自己的 CMakeLists.txt 文件就行。
编写方式也是大同小异。我们以 Module1
的 CMakeLists.txt 来举例:
set(PROJECT_NAME Module1)
project(${PROJECT_NAME})SOLUTION_DEFINITIONS_DEFAULT_SETTINGS()add_definitions(-DXXX_MODULE # 其中 {XXX_MODULE}参数填写 xxx_export.h 中的 define 对象-D${PROJECT_NAME}_EXPORTS # 注意这一行参数一般填写 `library_name + _EXPORTS`
)SOLUTION_COMPILE_OPTIONS_DEFAULT_SETTRINGS()include_directories("....")link_directories("....")# 添加所需的lib文件
link_libraries(....)# Common model
set(COMMON_MODULE_INC....h)set(COMMON_MODULE_SRC.....cpp)# HiWorld example
set(HI_WORLD_INC.....h)set(HI_WORLD_SRC.....cpp) set(INC${COMMON_MODULE_INC}${HI_WORLD_INC})set(SRC${COMMON_MODULE_SRC}${HI_WORLD_SRC})add_library(${PROJECT_NAME} SHARED ${INC} ${SRC})
target_link_libraries(${PROJECT_NAME} SecDevPrjDatabase)
set_target_properties(${PROJECT_NAME} PROPERTIES FOLDER "module1")# 添加解决方案分目录存放
SOURCE_GROUP("Common\\inc"FILES${COMMON_MODULE_INC})SOURCE_GROUP("Common\\src"FILES${COMMON_MODULE_SRC})SOURCE_GROUP("HiWorld\\inc"FILES${HI_WORLD_INC})SOURCE_GROUP("HiWorld\\src"FILES${HI_WORLD_SRC})set(startup_project ${PROJECT_NAME}) # 启动项只添加到一个工程中
IDE_DEBUG_ENVIRONMENT_DEFAULT_SETTING()
IDE_INSTALL_ENVIRONMENT_DEFAULT_SETTING()# 新增 install 时,同时拷贝资源配置文件的命令。
RESOURCE_AND_CONFIG_DEFAULT_SETTING()
至此,Cmake 的初始框架就基本搭建完成,后续的模块增加,无非是在这个框架基础上进行完善和填充。
总结
CMake 提供了一套强大的工具,能够帮助开发人员有效地管理和构建大型 C++ 项目。通过采用良好的组织结构,利用 CMake 的强大功能,如自动依赖处理、第三方库的集成等,可以极大地提高项目的可维护性和可扩展性。同时,CMake 的跨平台特性确保了项目可以在不同环境中轻松构建,从而促进了项目的可移植性。
通过本文的讨论,我们看到了使用 CMake 对于大型 C++ 项目管理的重要性,以及实施通用 CMake 框架的基本方法。希望这些信息可以帮助 C++ 开发者更好地理解和运用 CMake,从而提升其项目管理的效率和质量。