Windows 项目的 CMakeLists 编写

前言:

项目一直是以 .sln 解决方案打开和处理的,上传到 github 也是需要将 sln 文件包括到项目里,不太优雅(虽然方便),毕竟现在开源项目基本都是使用 CMake 做跨平台编译

因为项目是以 Windows 编译为主,就只写了 CMakeLists 中的 Windows 的部分,后续如果要跨平台的话,可以在此基础上拓展。

写这篇文章的主要目的是积累和分享学习经验,在寻找相关 vs 参数设置上花费了不少时间,写下来后可以让有类似需求的同学少走些弯路

正篇:

我也是最近才接触到 CMakeLists 的写法,从一开始的静态库和动态库的简单编写到现在整个项目都使用 CMake 编写,也算是一个循序渐进的过程

静态库和动态库的编写参考我过去的文章:

好了,开始吧

我们需要先设置 CMake 的最低版本,什么是最低版本,也就是说你添加 CMake 的函数时,有些函数是在特定的 CMake 版本后才被添加进来的,如果在该版本之前添加这些函数的话,CMake 会提示说找不到这些函数并报错

比如说,source_group 函数用于项目中组织源文件,将它们分组显示在 IDE 中,使项目结构更清晰,它是 CMake 2.8.11 中新添加进来的,所以就需要设置 CMake 的最低版本为 2.8.11

复制代码
cmake_minimum_required(VERSION 2.8.11)

一般情况下,我们会设置到 3.x 以后,这是通用的做法,因为有些函数会随着版本更新而添加新的参数或者功能

设置好最低 CMake 版本后,我们可能会需要设置 SDK 的最低版本,这里指的是 Windows 的 SDK,也是类似 CMake 的最低版本,有些你用的 win32 函数可能是新 SDK 添加进来的

复制代码
# Check for Win SDK version 10.0.19041 or above
if(MSVC AND MSVC_VERSION LESS 1920)
	message(STATUS "Windows API version is ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}")
	string(REPLACE "." ";" WINAPI_VER "${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}")

	list(GET WINAPI_VER 0 WINAPI_VER_MAJOR)
	list(GET WINAPI_VER 1 WINAPI_VER_MINOR)
	list(GET WINAPI_VER 2 WINAPI_VER_BUILD)

	set(WINAPI_COMPATIBLE FALSE)
	if(WINAPI_VER_MAJOR EQUAL 10)
		if(WINAPI_VER_MINOR EQUAL 0)
			if(WINAPI_VER_BUILD GREATER_EQUAL 19041)
				set(WINAPI_COMPATIBLE TRUE)
			endif()
		else()
			set(WINAPI_COMPATIBLE TRUE)
		endif()
	elseif(WINAPI_VER_MAJOR GREATER 10)
		set(WINAPI_COMPATIBLE TRUE)
	endif()

	if(NOT WINAPI_COMPATIBLE)
		message(FATAL_ERROR "missevan-fm requires Windows 10 SDK version 10.0.19041.0 and above to compile.\nPlease download the most recent Windows 10 SDK in order to compile (or update to Visual Studio 2019).")
	endif()
endif()

设置好 Windows SDK 最低版本后,还需要添加一些库的路径,比如我们可能会使用到 Qt 库,那么我们也可以继续添加 Qt 的路径

复制代码
set(CMAKE_PREFIX_PATH ${QTDIR})

if(QTDIR OR DEFINED ENV{QTDIR} OR DEFINED ENV{QTDIR32})
	# Qt path set by user or env var
	message(STATUS "Qt path is ${QTDIR}")
else()
	set(QTDIR "D:/Qt/5.15.x/msvc2019_x86_static")
	message(WARNING "QTDIR variable is missing.  Please set this variable to specify path to Qt (e.g. D:/Qt/5.15.x/msvc2019_x86_static)")
endif()

CMAKE_PREFIX_PATH 是 CMake 中一个用于指定查找第三方库、头文件、CMake 模块等路径的变量。它主要用于帮助 CMake 在特定路径下查找依赖项。

也就是说我们在 Windows 的环境变量中添加了 Qt 的路径,那么使用 CMAKE_PREFIX_PATH 可以直接找到 Qt 的路径并赋值给 QTDIR

如何找不到 Qt 的路径,那么我们就手动设置 Qt 的路径

说明一下,msvc2019_x86_static 是你编译 Qt 静态库的文件夹(用于商业要付费,建议链接到动态库),如何编译 Qt 的静态库,可以参考我之前的一篇文章,

接下来要设置 VS 的各个配置

复制代码
set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE)

复制代码
set(CMAKE_GENERATOR_PLATFORM Win32)
set(CMAKE_GENERATOR_TOOLSET "host=x86" CACHE STRING "Platform Toolset" FORCE)
set(CMAKE_MFC_FLAG 1)

复制代码
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd /Zi")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT /Zi")

Debug 和 Release 下生成 pdb 文件

复制代码
set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG} /DEBUG /OPT:REF /OPT:ICF")
set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /DEBUG /OPT:REF /OPT:ICF")

VS 配置这块的 CMake 的函数使用合起来如下

复制代码
message(STATUS "Set visual studio build params")
set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE)
set(CMAKE_GENERATOR_PLATFORM Win32)
set(CMAKE_GENERATOR_TOOLSET "host=x86" CACHE STRING "Platform Toolset" FORCE)
set(CMAKE_SUPPRESS_REGENERATION true)
set(CMAKE_MFC_FLAG 1)
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd /Zi")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT /Zi")
set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG} /DEBUG /OPT:REF /OPT:ICF")
set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /DEBUG /OPT:REF /OPT:ICF")
add_definitions(-D_UNICODE)

其中还有三个函数没说明,

  1. message 相当于 printf 函数,用于输出字符串,这边可以在 cmake 过程中观察执行到哪一步,方便定位错误

2. CMAKE_SUPPRESS_REGENERATION 是 CMake 中的一个变量,用于指定在生成项目文件(如 Visual Studio 的 .sln 和 .vcproj 文件)时是否抑制重新生成的行为。这个变量主要用于生成项目文件时的一些配置。

设置这个变量为 TRUE 时,可以告诉 CMake 在生成项目文件时避免强制重新生成已有的项目文件。这样做可以节省一些时间,特别是在一些场景下,比如在多次配置和生成过程中,确保不会因为每次重新生成项目文件而造成不必要的延迟。

3. add_definitions(-D_UNICODE) 是 CMake 中用于向编译器添加预定义宏的命令。在这里,-D 选项表示定义一个宏,_UNICODE 是一个预定义的宏,用于指示编译器使用 Unicode 字符集。

要注意的是,CMAKE_GENERATOR_PLATFORM 和 CMAKE_GENERATOR_TOOLSET 必须要在 Project 之前使用,否则不生效,使用 clang 编译的时候,就要加上条件语句用于区分

接着定义项目名

复制代码
project(my-project) 

这个最直接的作用就是 cmake 生成 my-project.sln 文件

设置 C++ 标准库版本

复制代码
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
  • set(CMAKE_CXX_STANDARD 14) 指定 C++ 标准为 C++14。这告诉 CMake 使用 C++14 标准来编译项目中的 C++ 代码。

  • set(CMAKE_CXX_STANDARD_REQUIRED ON) 表示要求编译器严格遵循所指定的 C++ 标准。如果编译器不支持所设定的 C++ 标准,编译将会失败。这有助于确保项目代码只使用所选定的标准语言特性。

  • set(CMAKE_CXX_EXTENSIONS OFF) 告诉 CMake 不要启用编译器的扩展特性。这可以确保编译器不会启用除 C++ 标准所定义之外的额外扩展功能。

之前配置完 Qt 的路径后,这时就要设置 moc 了,moc 其实就是 Qt 的反射机制,用于信号槽的连接

复制代码
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

设置版本号,版本号也就是 VS 中 rc 文件里的各个值

你需要使用 configure_file 函数将 .in 文件配置成 rc 文件

configure_file 是 CMake 中的一个命令,用于在构建过程中通过读取一个文件的内容,替换其中的变量,然后生成一个新的文件。

它的主要作用是处理项目中的模板文件,将其中的变量替换为相应的值,生成一个目标文件。这个命令通常用于生成配置文件,包含了项目编译时所需的特定信息,例如版本号、路径等。

其中 MP_VERSION_MAJOR 是变量,也可以理解为宏

复制代码
set(MP_VERSION 1.0.0)

string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" MP_VERSION_MATCH "${MP_VERSION}")

set(MP_VERSION_MAJOR ${CMAKE_MATCH_1})
set(MP_VERSION_MINOR ${CMAKE_MATCH_2})
set(MP_VERSION_REVISION ${CMAKE_MATCH_3})
# MP_BUILD_NUMBER 我们一般不设置
set(MP_BUILD_NUMBER "0")
set(MP_FILE_DESCRIPTION "XXX")
set(MP_INTERNAL_NAME "my-project.dll")
set(MP_LEGAL_COPYRIGHT "Copyright (C) xxx xxxx, Co., Ltd.")
set(MP_ORIGINAL_FILENAME "my-project.dll")
set(MP_PRODUCT_NAME "XXXXXXX")
set(MP_PRODUCT_VERSION "${MP_VERSION_MAJOR}.${MP_VERSION_MINOR}.${MP_VERSION_REVISION}.${MP_BUILD_NUMBER}")
set(MP_COMPANY_NAME "XXXXXXX有限公司")

比如说下面是我的 in 文件

复制代码
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
// Chinese (Simplified, PRC) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS)
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED

#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE 
BEGIN
    "resource.h\0"
END

2 TEXTINCLUDE 
BEGIN
    "#include ""winres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED


/////////////////////////////////////////////////////////////////////////////
//
// Icon
//

// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_MAIN                ICON                    "..\\img\\my_project.ico"


/////////////////////////////////////////////////////////////////////////////
//
// Version
//

VS_VERSION_INFO VERSIONINFO
 FILEVERSION ${MP_VERSION_MAJOR},${MP_VERSION_MINOR},${MP_VERSION_REVISION},${MP_BUILD_NUMBER}
 PRODUCTVERSION ${MP_VERSION_MAJOR},${MP_VERSION_MINOR},${MP_VERSION_REVISION},${MP_BUILD_NUMBER}
 FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
 FILEFLAGS 0x1L
#else
 FILEFLAGS 0x0L
#endif
 FILEOS 0x40004L
 FILETYPE 0x1L
 FILESUBTYPE 0x0L
BEGIN
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "080404b0"
        BEGIN
            VALUE "CompanyName", "${MP_COMPANY_NAME}"
            VALUE "FileDescription", "${MP_FILE_DESCRIPTION}"
            VALUE "FileVersion", "${MP_PRODUCT_VERSION}"
            VALUE "InternalName", "${MP_INTERNAL_NAME}"
            VALUE "LegalCopyright", "${MP_LEGAL_COPYRIGHT}"
            VALUE "OriginalFilename", "${MP_ORIGINAL_FILENAME}"
            VALUE "ProductName", "${MP_PRODUCT_NAME}"
            VALUE "ProductVersion", "${MP_PRODUCT_VERSION}"
        END
    END
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x804, 1200
    END
END

#endif    // Chinese (Simplified, PRC) resources
/////////////////////////////////////////////////////////////////////////////



#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//


/////////////////////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED

在此基础上,调用 configure_file

复制代码
configure_file(missevan-fm-kernel.rc.in ${CMAKE_CURRENT_SOURCE_DIR}/build/my_project.rc)

就可以转换为需要的 my_project.rc 文件了,这样做的好处就是在 CMake 中设置 rc 中的变量,特别是每次发版都要修改的版本号

同理,对于要修改版本号这种需求,都可以使用 in 文件转换来实现

这步配置完后,我们开始进入库的配置和代码文件的配置

使用 find_package 将 Qt 库加入到项目工程中,仅添加用到的 Qt 库,其他官方库亦是如此

复制代码
find_package(Qt5Core)
find_package(Qt5Gui)
find_package(Qt5Widgets)
find_package(Qt5WinExtras)

设置源码文件路径,一般我们都会将 .cpp .h 都放到 src 文件夹下,以便查找

这里我们的源码都是在 src 文件夹下,之所以定义 SRC_PATH,是下面添加每个 cpp 文件都要添加路径,这样节省了时间以及美观

复制代码
set(SRC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/src)

设置完 SRC_PATH 之后就可以设置各个源码子目录了,比如我们的代码层级是这样设置的

我们希望 CMake 生成的 .sln 工程在被打开也是呈现这样的目录,那么我们就需要使用 source_group 函数

先定义好 cpp 路径,我们需要手动将每个头文件和源文件都添加进来,有些人是使用比较快速的方法,就是只要后缀是 .h 和 .cpp 的文件都一键添加进来,这样做,有好有弊

我个人觉得不太好,因为实际过程中,有些 cpp 文件是过旧的,你不想删除但也不想添加到工程中,这时你如果一键添加,就不是预期的结果了

复制代码
set(AUDIO_SOURCES
	${SRC_PATH}/audio/1.cpp
	${SRC_PATH}/audio/2.cpp
	${SRC_PATH}/audio/3.cpp
)

set(AUDIO_HEADERS
	${SRC_PATH}/audio/1.h
	${SRC_PATH}/audio/2.h
	${SRC_PATH}/audio/3.h
)
复制代码
source_group("Source Files/audio" FILES ${AUDIO_SOURCES})
source_group("Header Files/audio" FILES ${AUDIO_HEADERS})
source_group("Source Files/base" FILES ${BASE_SOURCES})
source_group("Header Files/base" FILES ${BASE_HEADERS})
source_group("Source Files/live" FILES ${LIVE_SOURCES})
source_group("Header Files/live" FILES ${LIVE_HEADERS})
source_group("Source Files/net" FILES ${NET_SOURCES})
source_group("Header Files/net" FILES ${NET_HEADERS})
source_group("Source Files/ui" FILES ${UI_SOURCES})
source_group("Header Files/ui" FILES ${UI_HEADERS})

这样我们的源文件和头文件就设置好了

现在配置三方库的宏路径,一般情况下我们习惯将用到的三方库都放到一个文件夹,比如 third_party

复制代码
set(ADir ${CMAKE_CURRENT_SOURCE_DIR}/third_party/a)
set(BDir ${CMAKE_CURRENT_SOURCE_DIR}/third_party/b)
set(CDir ${CMAKE_CURRENT_SOURCE_DIR}/third_party/c)

接着设置预处理宏和预编译静态库

复制代码
set(MY_PROJECT_PLATFORM_DEPS 
	aaa.lib
	bbb.lib
	ccc.lib
)

set(COMPILE_DEFINITIONS
	UNICODE
	_UNICODE
	WIN32
	_NO_GDIPLUS_
	_CRT_RAND_S
	QT_CORE_LIB
	QT_GUI_LIB
	QT_WIDGETS_LIB
	QT_WINEXTRAS_LIB
	MP_VERSION="${MP_VERSION}"
)

注意预处理宏有个 MP_VERSION="${MP_VERSION}",这个实际上是将版本号由 CMakeLists 透传到项目中,这样我们就可以在项目中直接使用 MP_VERSION

复制代码
const char kAppVersion[] = MP_VERSION;

项目中我们还有两个自编译的三方库,需要使用 add_subdirectory 添加进来,这样做的目的有个好处就是,每次 CMake -B build 时,都会一同将子项目的 CMakeLists.txt 一同编译

使用 msbuild 编译 .sln 工程除了编译主项目也会将添加进来的子项目一同编译出来

注意:每个子项目也需要一个 CMakeLists.txt

我们的自编译的三方库都是编译成静态库

复制代码
add_subdirectory(${ADir}/A/)
add_subdirectory(${BDir}/B/)

如何编译成静态库,可以参考我的另一篇文章:

子项目的 CMakeLists.txt 位置就在 {ADir}/A/ 下面,像这样,{ADir}/A/CMakeLists.txt,${BDir}/B/CMakeLists.txt

设置完这些,使用 add_executable 添加可执行文件

复制代码
add_executable(my_project
	${AUDIO_SOURCES} ${AUDIO_HEADERS}
	${AUDIO_CAPTURES_SOURCES} ${AUDIO_CAPTURES_HEADERS}
	${BASE_SOURCES} ${BASE_HEADERS}
	${LIVE_SOURCES} ${LIVE_HEADERS}
	${NET_SOURCES} ${NET_HEADERS}
	${RESOURCE_FILES}
)

下一步我们开始设置工程的属性

工程中如果有自定义宏文件,如下,

对于这种文件,我们需要找到文件的位置,并使用 set_target_properties 添加到工程中

复制代码
set_target_properties(my_project PROPERTIES VS_USER_PROPS "${CMAKE_SOURCE_DIR}/projects/third_party.props")

其他的一些工程的属性设置也是类似

复制代码
# enable vcpkg
set_target_properties(missevan_fm_kernel PROPERTIES VS_GLOBAL_VcpkgEnabled true)
# Release 下生成 pdb 文件
set_target_properties(missevan_fm_kernel PROPERTIES LINK_FLAGS "/INCREMENTAL:NO /DEBUG /OPT:REF /OPT:ICF")

需要注意的是,VS_GLOBAL_VcpkgEnabled 只能设置 Use Vcpkg 选项

如果你要设置下面的 Triplet 和 Vcpkg Configuration 选项,目前没有办法做到,我是通过 powershell 脚本强行修改 vcxproj 文件做到修改 Triplet 的选项

powershell 脚本的编写,我后续会在另一篇文章中介绍

如果你还需要设置 .def 文件,可以使用 target_link_options 做到,def 文件你可以将它视为 dllexport 的等同效果,一般用来暴露工程的 main 函数

复制代码
if (CMAKE_BUILD_TYPE STREQUAL "Release")
	target_link_options(my_project PRIVATE /DEF:${CMAKE_CURRENT_SOURCE_DIR}/projects/my_project.def)
endif ()

接下来是添加头文件路径,使用 include_directories 或者 target_include_directories,target_include_directories 是比较现代的做法,官方是推荐使用后者的

不过我是用的前者,理由是使用 target_include_directories 时,总会给我添加一些稀奇古怪的路径

复制代码
include_directories(
	${SRC_PATH}/
	${SRC_PATH}/my_project/
	${FaacDir}/include/
	${ADir}/include/
	${BDir}/include/
	${CDir}/include/
	${QTDIR}/include/
	${QTDIR}/include/QtCore/
	${QTDIR}/include/QtGui/
	${QTDIR}/include/QtANGLE/
	${QTDIR}/include/QtWidgets/
	${QTDIR}/include/QtWinExtras/
)

添加库路径,使用 target_link_directories

复制代码
target_link_directories(missevan_fm_kernel PRIVATE 
	${ADir}/lib/
	${BDir}/lib/
	${CDir}/lib/    
    ${CMAKE_CURRENT_SOURCE_DIR}/build/DDir/$<CONFIG>/
	${QTDIR}/lib/
	${QTDIR}/plugins/imageformats/
	${QTDIR}/plugins/platforms/
	${QTDIR}/plugins/audio/
	${QTDIR}/plugins/styles/
)

{CMAKE_CURRENT_SOURCE_DIR}/build/DDir/<CONFIG>/ 这行说明一下

当我们使用 add_subdirectory 添加三分库时,因为分为 Debug 和 Release 调试模式,故指定的库路径也有所不同,比如 Debug 的库在 Debug 文件夹下,Release 的库在 Release 文件夹下

$<CONFIG> 会在两种模式下自动赋值为对应的调试模式,比如现在用的是 Debug 模式,那 \ 也会变成 Debug,不过对于 DebugDll 和 ReleaseDll,<CONFIG> 无法转换。

还有,target_link_directories 在 Windows 添加的路径很是奇葩

它会在每行路径下新添加一个 xxx/$(Configuration) 路径,搜了一下类似的案例,发现很多人都遇到过,是个顽疾

官方虽然开了 case 尝试修复,目前为止,仍然处于"怠工"阶段

接着添加三方依赖库,也就是 Link/Input->Additional Dependencies,使用 target_link_libraries

复制代码
target_link_libraries(my_project
	PRIVATE
	A
	B
	$<$<CONFIG:Debug>:
		libcmtd.lib
		Qt5Cored.lib
		Qt5Guid.lib
		Qt5Widgetsd.lib
		Qt5WinExtrasd.lib
	>
	$<$<CONFIG:Release>:
		Ole32.lib
		Shell32.lib
		Shlwapi.lib
		User32.lib
		Gdi32.lib
		Qt5Core.lib
		Qt5Gui.lib
		Qt5Widgets.lib
		Qt5WinExtras.lib
	>
)

其中 A 和 B 是使用 add_subdirectory 添加的子目录,这样做的好处是它会自动添加子库的依赖

接着添加预编译宏,使用 target_compile_definitions

复制代码
target_compile_definitions(my_project
	PRIVATE
	$<$<CONFIG:Debug>:
		QT_QML_DEBUG
		_DEBUG
		_CONSOLE
	>
	$<$<CONFIG:Release>:
		QT_NO_DEBUG
		NDEBUG
	>
)

接着设置链接器子系统,也就是 Windows 和 Console,一般 Debug 下使用 Console,因为需要弹出控制台查看调试信息,Release 下则是 Windows

注意 Console 对应的是 main 入口,Windows 对应的是 wWinmain 入口

使用 set_target_properties 即可

复制代码
set_target_properties(my_project PROPERTIES WIN32_EXECUTABLE $<$<CONFIG:Release>:TRUE>)

\<<CONFIG:Release>:TRUE> 表示只有 Release 下才为 TRUE,其余默认为 FALSE,FALSE 就是默认使用 Console

如果你需要在编译完后将 exe 依赖的三分库的 dll 挪到特定文件夹下,可以使用 add_custom_command 实现

在 vs 中,我们可以使用 Configuration Properties->Build Events->Post-Build Event,在 Command Line 输入 for 循环复制的伪代码

但是 CMake 中我们则需要上面提到的方法

复制代码
set(DLL_FILES
	${ADir}/a.dll
	${BDir}/b.dll
	${CDir}/c.dll
)

foreach(DLL ${DLL_FILES})
	if(CMAKE_BUILD_TYPE STREQUAL "Debug")
		add_custom_command(TARGET my_project POST_BUILD
			COMMAND ${CMAKE_COMMAND} -E copy ${DLL} ${CMAKE_CURRENT_SOURCE_DIR}/build/bin/$<CONFIG>/
		)
	elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
		add_custom_command(TARGET my_project POST_BUILD
			COMMAND ${CMAKE_COMMAND} -E copy ${DLL} ${CMAKE_CURRENT_SOURCE_DIR}/build/bin/MYPROJECT/${MF_VERSION}/
		)
	endif()
endforeach()

通过这种方法,在程序编译后会自动将 a.dll b.dll c.dll 复制到 {CMAKE_CURRENT_SOURCE_DIR}/build/bin/<CONFIG>/ 文件夹下

如果你需要覆盖某个文件内的内容,比如说版本号,可以借助 file 函数

复制代码
if (CMAKE_BUILD_TYPE STREQUAL "Release")
	set(DATA_VERSION_PATH ${CMAKE_CURRENT_SOURCE_DIR}/build/bin/MYPROJECT/data/VERSION)
	file(WRITE ${DATA_VERSION_PATH} ${MF_VERSION})
	message(STATUS "Set build version success.")
endif()  

其中 ${MF_VERSION} 是版本号

更新:vcpkg 可以不使用 powershell 脚本添加 vcpkg 选项,而是使用 TRIPLET 实现

复制代码
cmake ..  -DVCPKG_TARGET_TRIPLET=x86-windows-static

结尾:如果有跨平台需求的,可以参考开源项目的 CMakeKLists,比如说 OBS,它支持跨平台的构建,其中包含了很多脚本的编写

CMake 确实复杂,短时间内是无法理解透,起码目前为止很多 CMake 函数以及 CMake 中的脚本编写我还是懵懂状态,后续得继续学习才行啊