C++大型项目管理:通用CMake框架的架构奥秘

往期本博主的 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 两个子模块:Module1Module2;还依赖第三方件: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 直接指向内层的文件夹就行。

对于里面的 Module1Module2 ,都是到了具体的模块业务,只用编写自己的 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,从而提升其项目管理的效率和质量。

  • 32
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值