构建系统描述了如何使用构建工具从项目的源代码构建项目的可执行文件和库,从而自动执行该过程。例如,构建系统可以是用于命令行 make 工具的 Makefile 或用于集成开发环境的项目文件。为了避免维护多个这样的构建系统,项目可以使用以 CMake 语言编写的文件抽象地指定其构建系统。从这些文件中,CMake 通过称为生成器的后端为每个使用者在本地生成一个首选的构建系统。

另外,在 c++ 代码的跨平台使用中,包括增减文件在内,任何其他涉及项目工程的改动都要各个平台的开发同步处理各自平台的工程。而一旦某个平台没有及时更新项目文件就可能会导致 CI/CD 自动化流程失败,非常影响工作效率。

为避免费时费力地维护多个这样的构建系统,可以选择用 CMake 对其进行抽象。这样我们只需要维护一个 CMakeLists.txt,它就可以为你生成指定的 vcxproj 文件或者 makefile 文件。然后调用 MSBuild 或者 make 进行编译就可以。

另外我们可以把平台特定的参数放到单独的模块里,然后 include 到 CMakeLists.txt 文件中,这样可以保持比较清晰的文件结构。

编写生成 vcxproj 工程的 cmake 脚本

脚本中常常会夹杂着我们自定义的变量和 CMake 的保留变量,CMake 的保留变量一般是:

  1. 以 CMAKE_ 开头,无论大写小写或是混合
  2. 以 _CMAKE_ 开头,同上
  3. 以下划线开头,后面跟随任何 cmake 命令

CMakeLists.txt

# 设置 cmake 的最低版本
cmake_minimum_required(VERSION 3.25.0)

# (可选)设置 windows SDK 版本,要在 project 命令前设置完毕
if (WIN32)
    set(CMAKE_SYSTEM_VERSION 10.0.18362.0 CACHE STRING "" FORCE)
endif()

# 设置工程的名称为 TestProject,改为真实的工程名
project(TestProject)

# 接下来设置整个项目的根目录,后续工程内使用所有文件路径将以该路径为基准, 
# 后续的路径就可以用 WORKSPACE_PATH 指定,代替晦涩的相对路径
# PROJECT_SOURCE_DIR 是 cmake 内置的变量,
# 在调用 project() 命令后被初始化为当前 cmake 文件所在目录,
# 通常会用 -S 参数传给 cmake.exe
set(WORKSPACE_PATH "${PROJECT_SOURCE_DIR}/../..")

# 设置该工程使用的头文件搜索路径,此处有 public 和 private 两个路径,
# 这里涉及到作用域的概念,public 属性可以被使用这个的对象继承。
# 可以理解为如果某个属性(头文件、预定义宏、导入库等)需要被库的使用者继承就设为 public,反之用 private。
# 因为样例中仍然保持了 lib/so 级别的依赖而没有做 cmake 脚本级别的依赖,
# 所以在这里两个属性的意义不是很大。
# 后面 .cmake 文件中可以看到把两个变量设置给 cmake 生成环境。
# 多个路径时可以按每个一行输入,引号可以省略。
# PS:虽然是 private,但这里的路径仍然是全平台通用,特定平台的包含路径会写到下面的 .cmake 文件里
set(TARGET_COMMON_PUBLIC_INC_PATH "${WORKSPACE_PATH}/include/definitions")
set(TARGET_COMMON_PRIVATE_INC_PATH
${WORKSPACE_PATH}/src/internal_1/include
${WORKSPACE_PATH}/src/internal_2/include
${WORKSPACE_PATH}/src/utils/include
)

# 添加项目使用的 cpp/h 文件,这里放所有平台都使用的公共文件,
# 特定平台的文件我们可以放到后面的 .cmake 平台相关文件中。
# 头文件也应该列进来。
set(TARGET_COMM_SRC_LIST
${WORKSPACE_PATH}/include/definitions/header_1.h
${WORKSPACE_PATH}/src/internal_1/xxx.cpp
${WORKSPACE_PATH}/src/internal_1/xxx.h
...
)

# 使用 include 命令包含平台相关文件
if (WIN32)
  include(module CMakeLists.win.cmake)
elseif ("${CMAKE_SYSTEM_NAME}" STREQUAL "Linux")
    include(CMakeLists.linux.cmake)     
endif()

CMakeLists.win.cmake

# 同样设置最低版本
cmake_minimum_required(VERSION 3.25.0)

# 定义生成的文件名,这里只是定义变量,后面还要用 add_library 设置
set(TARGET_NAME TestProject)

# 导入 CMakeLists.txt 中已经定义的变量 
set(WORKSPACE_PATH "${WORKSPACE_PATH}")
set(TARGET_COMM_SRC_LIST "${TARGET_COMM_SRC_LIST}")
set(TARGET_COMMON_PUBLIC_INC_PATH "${TARGET_COMMON_PUBLIC_INC_PATH}")
set(TARGET_COMMON_PRIVATE_INC_PATH "${TARGET_COMMON_PRIVATE_INC_PATH}")

# CMAKE_BUILD_TYPE 一般是 Debug / Release / RelWithDebInfo / MinSizeRel,赋一个默认值以防未指定
# STRING 后面的 "" 是描述字符串,可以设为空
if(NOT DEFINED CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
endif()

# CMAKE_CONFIGURATION_TYPES 是给像 VS、xcode 这类一个工程里包含多个编译配置的工程用的,
# 上面 CMAKE_BUILD_TYPE 是给单配置工程如 Makefile 用
set(CMAKE_CONFIGURATION_TYPES "${CMAKE_BUILD_TYPE}" CACHE STRING "" FORCE)

# 自定义一个小写的配置字符串变量“release”,设定一个默认值
# 根据实际情况使用
if(NOT DEFINED TARGET_BUILD_CFG_TYPE)
    set(TARGET_BUILD_CFG_TYPE Release)
endif()
string(TOLOWER ${TARGET_BUILD_CFG_TYPE} LOWER_TARGET_BUILD_CFG_TYPE)

# CMAKE_GENERATOR_PLATFORM 通过 -A 命令行参数传给 cmake.exe
# 生成一个小写的平台名,如 win32/x64/arm64
# GEN_PLATFORM 用来控制不同平台编译产物的输出路径,根据实际情况使用
string(TOLOWER ${CMAKE_GENERATOR_PLATFORM} LOWER_GENERATOR_PLATFORM)
set(GEN_PLATFORM ${LOWER_GENERATOR_PLATFORM})
if (${GEN_PLATFORM} STREQUAL "win32")
    set(GEN_PLATFORM "")
endif()

# PS:如果工程中有多种配置,要根据配置生成动态库或静态库,可以用下面的代码
# 会将生成类型记到 TARGET_LIB_TYPE 变量中
# set(TARGET_BUILD_TYPE_LIST "debug;static_release;release") 
# set(TARGET_LIB_TYPES SHARED STATIC SHARED) 
# string(TOLOWER ${TARGET_BUILD_CFG_TYPE} LOWER_TARGET_BUILD_CFG_TYPE) 
# list(FIND TARGET_BUILD_TYPE_LIST ${LOWER_TARGET_BUILD_CFG_TYPE} bt_idx) 
# if(bt_idx EQUAL -1) 
#   message(FATAL_ERROR "Unknown build configuration") 
# endif() 
# list(GET TARGET_LIB_TYPES ${bt_idx} TARGET_LIB_TYPE)

# 指明生成的是库文件而不是 exe(exe 要用 add_executable),类型有 SHARED、STATIC、MODULE,默认是 STATIC。
# 如果是生成 windows 的资源 dll 或者是托管 dll 时要用 MODULE,
# 因为 cmake 使用 SHARED 类型时会假定它一定导出符号。
# 这里只会生成动态 dll 所以直接填 SHARED
add_library(${TARGET_NAME} SHARED)

# 设置编译器特性,使用什么 c++ 标准,还可以开启/关闭里面某些特定特性
# TARGET_NAME 一定是用 add_executable() 或 add_library() 设置过的名称
target_compile_features(${TARGET_NAME} PRIVATE cxx_std_14)

# 设定目标的特定属性,这里是开启全程序优化
set_target_properties(${TARGET_NAME} PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE)

# 添加平台特定的文件,用一个变量保存
set(TARGET_PLATFORM_SRC_FILES 
${WORKSPACE_PATH}/src/internal_1/file_for_win.cpp 
${WORKSPACE_PATH}/src/internal_1/file_for_win.rc
${WORKSPACE_PATH}/src/internal_1/win_resource.h
)

# 将前面的公共文件以及平台特定文件添加到编译列表中
target_sources(${TARGET_NAME} PRIVATE "${TARGET_COMM_SRC_LIST}" "${TARGET_PLATFORM_SRC_FILES}")

# 设置头文件的包含路径,这里 TARGET_INC_PATH_LIST 是平台特定的包含路径,显然是 private 的
set(TARGET_INC_PATH_LIST "") 
target_include_directories(${TARGET_NAME} 
PUBLIC "${TARGET_COMMON_PUBLIC_INC_PATH}"
PRIVATE "${TARGET_COMMON_PRIVATE_INC_PATH}" "${TARGET_INC_PATH_LIST}")

# 设置导入库的包含路径,其实更推荐给每个导入库指定全路径,防止错误引用到其他的同名库上。
# 在下面 TARGET_LINK_LIBS_LIST 部分就可以看到每个导入库都带有路径。
#if (${LOWER_TARGET_BUILD_CFG_TYPE} STREQUAL "release") 
#    set(TARGET_LINK_PATH_LIST "${WORKSPACE_PATH}/bin/${GEN_PLATFORM}/${TARGET_BUILD_CFG_TYPE}/lib;${WORKSPACE_PATH}/bin/${GEN_PLATFORM}/${TARGET_BUILD_CFG_TYPE}") 
#endif()
#target_link_directories(${TARGET_NAME} PRIVATE "${TARGET_LINK_PATH_LIST}")

# 添加导入库,通过不同编译配置控制生成路径
set(TARGET_LINK_LIBS_LIST
${WORKSPACE_PATH}/bin/${GEN_PLATFORM}/${TARGET_BUILD_CFG_TYPE}/jsoncpp.lib;
${WORKSPACE_PATH}/bin/${GEN_PLATFORM}/${TARGET_BUILD_CFG_TYPE}/libprotobuf-lite.lib
)
target_link_libraries(${TARGET_NAME} PRIVATE "${TARGET_LINK_LIBS_LIST}")

# 设置预定义宏 
if(${LOWER_TARGET_BUILD_CFG_TYPE} STREQUAL "debug")
    set(TARGET_PREDEFINE_MACRO_LIST "WIN32;_DEBUG;_WINDOWS;_USRDLL;_UNICODE;UNICODE") 
elseif(${LOWER_TARGET_BUILD_CFG_TYPE} STREQUAL "release")    
    set(TARGET_PREDEFINE_MACRO_LIST "WIN32;NDEBUG;_WINDOWS;_USRDLL;_UNICODE;UNICODE") 
    if (${LOWER_GENERATOR_PLATFORM} STREQUAL "arm64")
        list(APPEND TARGET_PREDEFINE_MACRO_LIST _ATL_NO_OPENGL)
    endif()
endif()
target_compile_definitions(${TARGET_NAME} PRIVATE "${TARGET_PREDEFINE_MACRO_LIST}")

# 设置额外的编译选项,
# 对于 vs 工程可以打开工程属性 - "C/C++" - 命令行,将所有参数复制过来,删掉含有路径的命令参数,
# 再删掉 /D 参数(预定义宏,上面刚刚定义过)就可以了
# 当然稳妥起见,可以在用 cmake 生成出 vcxproj 后再和原来的工程对比一下
if(${LOWER_TARGET_BUILD_CFG_TYPE} STREQUAL "debug")
    if(${LOWER_GENERATOR_PLATFORM} STREQUAL "win32")
        set(TARGET_BUILD_CFG_LIST /MP /GS /analyze- /W3 /Zc:wchar_t /ZI /Gm- /Od /Zc:inline /fp:precise /errorReport:prompt /WX- /Zc:forScope /RTC1 /Gd /Oy- /MDd /FC /EHsc /nologo /diagnostics:column)
    endif()
elseif(${LOWER_TARGET_BUILD_CFG_TYPE} STREQUAL "release")
    if (${LOWER_GENERATOR_PLATFORM} STREQUAL "arm64")
        set(TARGET_BUILD_CFG_LIST /MP /GS /GL /analyze- /W3 /Gy /Zc:wchar_t /guard:cf /Zi /Gm- /O1 /Zc:inline /fp:precise /errorReport:prompt /WX- /Zc:forScope /Gd /Oy- /Oi /MD /FC /EHsc /nologo /Os /diagnostics:column /Oy- /bigobj)
    elseif(${LOWER_GENERATOR_PLATFORM} STREQUAL "win32")
        set(TARGET_BUILD_CFG_LIST /MP /GS /GL /analyze- /W3 /Gy /Zc:wchar_t /guard:cf /Zi /Gm- /O1 /Zc:inline /fp:precise /errorReport:prompt /WX- /Zc:forScope /Gd /Oy- /Oi /MD /FC /EHsc /nologo /Os /diagnostics:column /Oy-)
    else() # x64
        set(TARGET_BUILD_CFG_LIST /MP /GS /GL /W3 /Gy- /Zc:wchar_t /guard:cf /Zi /Gm- /O1 /Zc:inline /fp:precise /errorReport:prompt /WX- /Zc:forScope /Gd /Oi /MD /FC /EHsc /nologo /Os /diagnostics:column /Oy-)
    endif()
endif()
target_compile_options(${TARGET_NAME} PRIVATE ${TARGET_BUILD_CFG_LIST})

# 设置额外的链接选项
# 同上,打开工程属性 - 链接器 - 命令行,复制所有并删除带有路径的参数,
# 生成后再和原工程的参数做对比
if(${LOWER_TARGET_BUILD_CFG_TYPE} STREQUAL "debug")
    if(${LOWER_GENERATOR_PLATFORM} STREQUAL "win32")
        set(TARGET_INK_OPT_LIST /DEBUG /SAFESEH /INCREMENTAL /SUBSYSTEM:WINDOWS)
    else()
        set(TARGET_INK_OPT_LIST /DEBUG /INCREMENTAL /SUBSYSTEM:WINDOWS)
    endif()
elseif(${LOWER_TARGET_BUILD_CFG_TYPE} STREQUAL "release")
    if (${LOWER_GENERATOR_PLATFORM} STREQUAL "arm64")
        set(TARGET_INK_OPT_LIST /LTCG /DEBUG /OPT:REF /INCREMENTAL:NO /SUBSYSTEM:WINDOWS /OPT:ICF /guard:cf)
    elseif(${LOWER_GENERATOR_PLATFORM} STREQUAL "win32")
        set(TARGET_INK_OPT_LIST /DEBUG /OPT:NOREF /SAFESEH /INCREMENTAL:NO /SUBSYSTEM:WINDOWS /OPT:NOICF /guard:cf /LTCG)
    else() # x64
        set(TARGET_INK_OPT_LIST /LTCG:incremental /DEBUG /OPT:REF /INCREMENTAL:NO /SUBSYSTEM:WINDOWS /OPT:ICF /guard:cf)
    endif()
endif()
target_link_options(${TARGET_NAME} PRIVATE ${TARGET_INK_OPT_LIST})

# 设置编译产物的输出目录,一般有
#           Windows      Linux
# RUNTIME  .exe、.dll  可执行文件
# LIBRARY      -         .so
# ARCHIVE    .lib        .a
if (${LOWER_TARGET_BUILD_CFG_TYPE} STREQUAL "debug" OR ${LOWER_TARGET_BUILD_CFG_TYPE} STREQUAL "release")
    set(TARGET_RUNTIME_OUTPUT ${WORKSPACE_PATH}/bin/${GEN_PLATFORM}/${CONF_TYPE}/)
    set(TARGET_LIBRARY_OUTPUT ${WORKSPACE_PATH}/bin/${GEN_PLATFORM}/${CONF_TYPE}/lib)
    set(TARGET_ARCHIVE_OUTPUT ${WORKSPACE_PATH}/bin/${GEN_PLATFORM}/${CONF_TYPE}/lib)
endif()
set_target_properties(${TARGET_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${TARGET_RUNTIME_OUTPUT})
set_target_properties(${TARGET_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${TARGET_LIBRARY_OUTPUT})
set_target_properties(${TARGET_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${TARGET_ARCHIVE_OUTPUT})

# 设置 pdb 路径 
set_target_properties(${TARGET_NAME} PROPERTIES PDB_OUTPUT_DIRECTORY ${WORKSPACE_PATH}/bin/${GEN_PLATFORM}/${CONF_TYPE}/)

# 设置源代码文件如何摆放在解决方案资源管理器中的逻辑文件夹,即 .filter 文件的效果
source_group("source_file" FILES 
${WORKSPACE_PATH}/src/app1/sample.h 
${WORKSPACE_PATH}/src/app2/sample.h 
)

source_group("source_file/subs" FILES 
${WORKSPACE_PATH}/src/app1/sub1/sample2.h
${WORKSPACE_PATH}/src/app1/sub1/sample2.cpp
)

source_group("" FILES
${WORKSPACE_PATH}/src/res/resource.h 
${WORKSPACE_PATH}/src/res/resource.rc 
)

source_group("header_files" FILES 
${WORKSPACE_PATH}/inc/sample1.h 
${WORKSPACE_PATH}/inc/sample2.h 
)

source_group("pch" FILES
${WORKSPACE_PATH}/src/stdafx.cpp
)

生成&编译

使用如下命令调用 cmake.exe 进行生成,假设当前目录就是 CMakeLists.txt 所在目录。

set blt_arch_type=x64
set blt_cfg_type=release
cmake -B .\cmake\%blt_arch_type%\%blt_cfg_type% -S . -A %blt_arch_type% -D TARGET_BUILD_CFG_TYPE=%blt_cfg_type%

成功生成工程文件后再调用 MSBuild.exe 进行编译

if "%VSCMD_VER%" EQU "" (
    echo "init msvc env"
    call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\Tools\VsDevCmd.bat"
)
MSBUILD %~dp0\cmake\%blt_arch_type%\%blt_cfg_type%\TestProject.vcxproj /t:rebuild /p:Configuration="Release";Platform=%blt_arch_type%;BuildProjectReferences=false /m