往期本博主的 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.exe
VS_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.bat
DESTINATION ${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,从而提升其项目管理的效率和质量。