使用cmake构建visual studio项目

之前在写c++项目的时候,一直使用的是宇宙第一IDE Visual Studio,vs确实默默在后台做了许多事情,你只用管写代码其他的都不用操心。但是当项目引入大量的第三方库的时候,许多构建相关的配置就会比较麻烦。考虑到Rigel(一个巨大的轮子)这个项目后续需要支持多平台构建,所以打算试试使用cmake组织整个工程。

对于cmake使用其实没有太多经验的,主要是之前在编译一些第三方库的时候,libpng freetype之类的。一行命令行脚本 cmake ./ 然后自动生成了sln项目,后面就是熟悉的visual studio了。

cmake初体验

创建cmake工程

在项目目录下创建一个CMakeLists.txt创建好了一个cmake工程。

1
2
3
4
#指定cmake的最小版本
cmake_minimum_required(VERSION 3.2)
#制定项目的名称
project(Rigel)

在一个大型的项目中可能会有很多子工程,就类似于vs中的解决方案和项目的关系。可以对每个子项目指定一个CMakeLists.txt文件,子项目可以是一个静态库动态库或者是可执行程序等等。

在主的CMakeLists文件中添加add_subdirectory([sub folder path]) 就可以引入子工程的配置。

配置生成的项目

接下来就要添加项目了
add_executable([target name] [source files...]) add_executable可以为项目添加一个可执行工程
add_library([target name] [source files...]) 对于项目中静态库和动态库可以使用add_library进行添加

如果项目只有一个main.cpp 那么add_executable(testproj main.cpp),生成的sln中就会把main.cpp添加到testproj这个项目的源文件下面。

项目依赖配置
我们可以指定一个target依赖其他的库项目的target
add_dependencies(RigelEditor rgcore)

多源文件的配置

如果有多个源文件可以直接在source files里面添加add_executable(testproj xxx1.cpp xxx2.cpp...)

当然可以使用aux_source_directory([Path] [Variable])命令来获取一个目录下的的所有源文件 然后储存在变量Variable

还可以使用FILE() 方法来获取
几个简单的例子

1
2
FILE(GLOB HEAD_RIGEL_EDITOR ./include/*.h)  #GLOB 获取./include/目录下的*.h 储存在变量HEAD_RIGEL_EDITOR中
FILE(GLOB_RECURSE SRCS_RIGEL_EDITOR ./source/*.cpp)#GLOB 递归获取./include/目录和子目录下的*.cpp 储存在变量HEAD_RIGEL_EDITOR中

add_executable(testproj ${HEAD_RIGEL_EDITOR} ${SRCS_RIGEL_EDITOR})
这样我们就可以给项目添加多个源文件了

CMAKE生成的sln工程中不会把源文件拷贝到build的目录中,而是通过引用文件的方式

引用路径与库目录

我们添加了一个子项目后还需要设置include的路径,来添加对头文件的引用。
target_include_directories(RigelEditor PUBLIC ./include/rgeditor)
这样就给RigelEditor这target添加了一个include的路径./include/rgeditor

添加link的库文件
target_link_libraries(RigelEditor rgcore)
如果需要添加第三方的库文件,可以直接引用库文件的路径。

需要注意的是使用CMAKE_SOURCE_DIR可以获取到源文件的目录而不是生成的目录。

target_link_libraries(rgcore ${THIDPART_LIB_DIR}\\freetype\\libs\\x64\\freetype28.lib)

cmake其他配置

源文件分组

在visual studio中我们可以添加筛选器给源文件进行分组。cmake提供了source_group()来支持这一特性。

source_group需要写在add_library或者add_executable之后
下面是使用的示例:

1
2
3
4
5
6
7
8
9
10
11
12
FILE(GLOB SRCS_RGGRAPHICS_CORE ./source/*.cpp)
FILE(GLOB SRCS_RGGRAPHICS_DIRECTX11 ./source/directx11/*.cpp)

FILE(GLOB HEAD_RGGRAPHICS_CORE ./include/rggraphics/*.h)
FILE(GLOB HEAD_RGGRAPHICS_DIRECTX11 ./include/rggraphics/directx11/*.h)

add_library(rggraphics STATIC ${HEAD_RGGRAPHICS} ${SRCS_RGGRAPHICS})

source_group("source\\core" FILES ${SRCS_RGGRAPHICS_CORE})
source_group("source\\directx11" FILES ${SRCS_RGGRAPHICS_DIRECTX11})
source_group("header\\core" FILES ${HEAD_RGGRAPHICS_CORE})
source_group("header\\directx11" FILES ${HEAD_RGGRAPHICS_DIRECTX11})

set_property(GLOBAL PROPERTY USE_FOLDERS ON)打开USE_FOLDERS的配置

cmake source_group

判定编译的Architecture

有时候我们的配置需要区别64位与32位,如一些静态库动态库的版本不同,就需要在cmake中判断当前配置是否是64位。
CMAKE提供了CMAKE_CL_64这个变量来判定

1
2
3
4
5
if(CMAKE_CL_64)
#link 64位库文件
else()
#link 32位库文件
endif()

Configuration Debug/Release的判定

有时候我们需要区别Debug/Release build
$<$CONFIG:DEBUG:${BUILD_DIR_RIGELEDITOR_DEBUG}>$<$CONFIG:RELEASE:${BUILD_DIR_RIGELEDITOR_RELEASE}>
这个值在Debug的配置下输出${BUILD_DIR_RIGELEDITOR_DEBUG}在Release下则是
${BUILD_DIR_RIGELEDITOR_RELEASE}

指定build输出目录

生成sln会给每个子项目生成自己的输出路径一般情况下是
$(ProjectDir)$(PlatformTarget)$(Configuration)
TestProj/x64/Debug这样的路径
在cmake中我们可以制定一个target生成的路径便于我们执行post-build的脚本来做一些处理

1
2
3
4
set_target_properties(RigelEditor PROPERTIES 
RUNTIME_OUTPUT_DIRECTORY_DEBUG ${BUILD_DIR_RIGELEDITOR_DEBUG}
RUNTIME_OUTPUT_DIRECTORY_RELEASE ${BUILD_DIR_RIGELEDITOR_RELEASE}
)

自定义命令处理post-build

项目生成之后如果有文件需要拷贝,在vs中可以使用生成事件-后期生成事件-命令行来添加命令。cmake中也可以指定这样的生成事件的配置。

add_custom_command(TARGET RigelEditor POST_BUILD COMMAND xcopy ARGS ${CMD_COPY_DATA})
这样指定了RigelEditor在Post Build阶段执行 xcopy的命令行 参数为 ${CMD_COPY_DATA}

生成sln之后再后期生成事件中就可以看到cmake添加的命令

1
2
3
4
5
6
7
8
9
setlocal
xcopy "F:/_Proj_RgEngine/ProjRigel/Rigel\resources\Data" "F:/_Proj_RgEngine/ProjRigel/Build/win64\Build\RigelEditor_Debug\Data\" /E /Y
if %errorlevel% neq 0 goto :cmEnd
:cmEnd
endlocal & call :cmErrorLevel %errorlevel% & goto :cmDone
:cmErrorLevel
exit /b %1
:cmDone
if %errorlevel% neq 0 goto :VCEnd

cmake杂项

###VC++ Unicode支持
在vc++中定义UNICODE_UNICODE这两个宏,字符串就自动使用了宽字节版本的。所以我们需要在cmake中设定宏,否则编译会报错,使用visual stuido直接创建项目时会直接帮我们设置的好(再一次赞美下VisualStudio)
add_definitions(-DUNICODE -D_UNICODE)

###Visual Studio中配置预编译头
vs中通过配置预编译头文件加快项目的编译速度。可以通过设置Compile Flags /Yc /Yu添加预编译头的配置

使用CMake进行构建时,set_source_files_properties命令可以对源文件添加编译器参数

为了给多个子项目都配置预编译头文件,使用CMake的macro来批量处理。
定义两个macro set_pch use_pch

1
2
3
4
5
6
7
8
macro(set_pch VARCPP VARHEADER)
set_source_files_properties(${VARCPP} PROPERTIES COMPILE_FLAGS "/Yc${VARHEADER}")
endmacro(set_pch)
macro(use_pch VARSOURCES VARHEADER)
foreach( src_file ${VARSOURCES})
set_source_files_properties(${src_file} PROPERTIES COMPILE_FLAGS "/Yu${VARHEADER}")
endforeach( src_file ${VARSOURCES})
endmacro(use_pch VARHEADER VARSOURCES)

使用方式
假设预编译头文件为stdafx.h则所有的.cpp文件除了stdafx.cpp需要设置 /Yustdafx.h,使用预编译头。
stdafx.cpp设置 /Ycstdafx.h,生成预编译头。

1
2
use_pch("${SRCS_RGCORE}" "rgcore.h")
set_pch(./source/rgcore.cpp "rgcore.h")

一个常见的错误 调用cmake中的方法与宏时 如果需要传入的参数为数组 正确的写法应该是"${VARNAME}" 而不是${VARNAME}

总结

大概花了半天的时间将Rigel 从sln迁移到了cmake来构建。上文大概就是在配置过程中遇到的坑以及解决的方法的总结,提及到的cmake命令可以满足基本的项目构建了。
cmake使用起来感觉确实很方便,比起直接设置vs sln中的配置之类的,后续应该就会都用cmake了吧。


更新记录

  • 2017-10-04 文章发布
  • 2017-10-13 添加了Visual Stdudio 预编译头配置