命令

源文件收集

一般情况下,我们通过逐个列出的方式,设定源文件集(set(SRC_LIST src/main.c src/other.c))。这样的好处是可以明确控制那些文件会被加入到工程中。 但是有些时候,如果源文件较多,一个一个列出来的话可能会有些麻烦,对于这种情况,可以使用CMake提供的file命令来自动收集源文件。使用方法如下。

file(GLOB_RECURSE SRC_CORE src/*.c)

通过上面这行代码,会生成一个源文件列表,每一项是一个源文件的全路径。可以通过下面的代码来剔除一些不想加入工程的源文件。

foreach(rm_file ${SRC_CORE})
	string(REGEX MATCH ".*/filename1.c|.*/filename2.c" need_remove_file ${rm_file})
	if(need_remove_file)
		list(REMOVE_ITEM SRC_CORE ${need_remove_file})
	endif(need_remove_file)
endforeach(rm_file)

上面使用了CMake的正则表达式,匹配两个文件,分别为.*/filename1.c.*filename2.c,其中.*代表了任意字符串,filename1.c是文件名。

同一个工程同时生成静态库和动态库

有了如标题所述的需求,在网上搜索了一下,发现大部分博客都是讲通过设置OUTPUT_NAME,使两个工程(一个动态库,一个静态库)输出同名二进制文件。但是我不希望同样的源码,要创建两个工程。 可以使用下面的方法为使用一个工程同时生成动态库和静态库。

# 添加控制选项
# BUILD_SHARED_LIBS 是CMake内置变量
# 针对WIN32可以

if(WIN32)
	option(BUILD_SHARED_LIBS "Build Shared libs" ON)
	option(BUILD_AS_DLL "Build as dll" ${BUILD_SHARED_LIBS})
endif()

add_library(libHello ${SRC_LIST})
target_link_libraries(libHello ${EXTRA_LIB})

if(BUILD_AS_DLL)
	set_target_properties(libHello PROPERTIES COMPILE_DEFINITIONS BUILD_AS_DLL)
endif()

设置头文件目录和静态库

很多时候,我们会使用第三方库,因此,我们要在工程里面包含第三方库的头文件,链接第三方提供的静态链接库(如果有的话)。

# 包含头文件
# include_directories 具有继承性
# 当前 CMakeLists 里面包含的目录,会被 add_subdirectory 里面的 CMakeLists 继承
include_directories ( "D:/Program Files (x86)/Lua/include" )

# 库目录
# eg. 链接 Linux 下的 X11
link_directories(/usr/include/X11)
SET(SYS_LIB_LIST "x11")

# 链接库
set(LUA_LIB "D:/Program Files (x86)/Lua/lib/lua51.lib")
target_link_libraries(target ${LUA_LIB})

生成配置头文件

一般可以用CMake来进行平台检测,那么CMake的检测结果如何告诉C++工程呢?CMake提供了configure_file命令。 configure_file命令输入一个文本文件,处理文本文件中包含的CMake的指令,然后生成一个文本文件。

configure_file(
	"${PROJECT_SOURCE_DIR}/platform/config.h.in"
	"${PROJECT_BINARY_DIR}/config.h"
	)
/*
	config.h.in 文件
*/
#cmakedefine PLATFORM_WIN32
#cmakedefine PLATFORM_LINUX

如果 CMakeLists.txt 中定义了PLATFORM_WIN32变量,那么#cmakedefine PLATFORM_WIN32就会变成C++的宏定义语句#define PLATFORM_WIN32

生成Visual Studio筛选器

可以通过一个宏来为收集的文件建立筛选器。代码如下:

# file(GLOB_RECURSE all_files *.*)
# create_filters(all_files)
# add_executable(app ${all_files})

macro(create_filters source_files)
	if(MSVC)
		# 获取当前目录
		set(current_dir ${CMAKE_CURRENT_SOURCE_DIR})
		foreach(src_file ${${source_files}})
			# 求出相对路径
			string(REPLACE ${current_dir}/ "" rel_path_name ${src_file})
			# 删除相对路径中的文件名部分
			string(REGEX REPLACE "(.*)/.*" \\1 rel_path ${rel_path_name})
			# 比较是否是当前路径下的文件
			string(COMPARE EQUAL ${rel_path_name} ${rel_path} is_same_path)
			# 替换成Windows平台的路径分隔符
			string(REPLACE "/" "\\" rel_path ${rel_path})
			if(is_same_path)
				set(rel_path "\\")
			endif(is_same_path)

			# CMake 命令
			source_group(${rel_path} FILES ${src_file})
		endforeach(src_file)
	endif(MSVC)
endmacro(create_filters)

编译选项

Linux使用 C++ 11

在Linux下编译下面的代码时出错。

typedef std::map<key, value> Dict_Map;
Dict_Map::iterator it = DictMap.begin();
while(it != DictMap.end())
{
	it = DictMap.erase(it);
}

出错提示是erase函数没有返回值。 查看源码,发现#if __cplusplus >= 201103L时,iterator erase(const_iterator __position)。否则,void erase(iterator __position)。 改成使用 C++ 11 就好了。

# Use C++11 in linux
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=gnu++0x")

MSVC 禁用特定警告

一般情况下,不要禁用任何警告,而应该积极解决每一个警告。 但是有些情况下,当你充分了解某些警告可能造成的后果时,也可以禁用该警告。 比如要禁用警告4819(该文件包含不能在当前代码页中表示的字符),可以采用下面的语句:

if(MSVC)

SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4819")

endif()

调试运行

Visual Studio设置工作目录

使用Visual Studio开发的时候,假设编译构建生成的程序在binarypath/bin/debug目录下。我们调试时,会有一些数据要使用,假设这些数据在binarypath/data目录下,如果程序查找数据文件时,只在当前目录下查找,那么在这种情况下,就无法找到数据。要解决这个问题,可以在Visual Studio中设置工作目录,这样程序就能找到数据文件。 Visual Studio的工作目录设置是保存在projectname.vcxproj.user文件中,CMake没有提供专门的命令来设置工作目录,但是我们可以通过类似配置文件的方式来手动生成projectname.vcxproj.user文件。 perconfig.vcxproj.user.in

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='@USERFILE_CONFIGNAME@|@USERFILE_PLATFORM@'">
	<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
	<LocalDebuggerCommandArguments>@USERFILE_COMMAND_ARGUMENTS@</LocalDebuggerCommandArguments>
	<LocalDebuggerWorkingDirectory>@USERFILE_WORKING_DIRECTORY@</LocalDebuggerWorkingDirectory>
</PropertyGroup>

vcxproj.user.in

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="@USERFILE_VC_VERSION@" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
@USERFILE_CONFIGSECTIONS@
</Project>

CMake 模块

file(READ "${_launchermoddir}/perconfig.vcxproj.user.in" _perconfig)
set(USERFILE_CONFIGSECTIONS)
# 对于每一个配置(Debug, Release, RelWithDebInfo, MinSizeRel),生成对应的配置项
foreach(USERFILE_CONFIGNAME ${CMAKE_CONFIGURATION_TYPES})
	string(CONFIGURE "${_perconfig}" _temp @ONLY ESCAPE_QUOTES)
	string(CONFIGURE
		"${USERFILE_CONFIGSECTIONS}${_temp}"
		USERFILE_CONFIGSECTIONS
		ESCAPE_QUOTES)
endforeach()

configure_file("${_launchermoddir}/vcxproj.user.in"
	${VCPROJNAME}.vcxproj.user
	@ONLY)

perconfig.vcxproj.user.in中出现的@USERFILE_CONFIGNAME@@USERFILE_PLATFORM@@USERFILE_PLATFORM@@USERFILE_COMMAND_ARGUMENTS@@USERFILE_WORKING_DIRECTORY@等,都是在CMake模块中预先设置好的变量,通过string(CONFIGURE "${_perconfig}" _temp @ONLY ESCAPE_QUOTES)语句填到配置里面去的。

拷贝运行时库

对于需要动态链接的库,一般是直接在可执行程序目录下查找的,所以对于项目中使用的第三方动态链接库,我们需要在用CMake构建项目的时候,将这些动态链接库拷贝到最终可执行文件目录下。

拷贝动态链接库,用于调试时运行。

macro(copy_dll depdir dllname)
	foreach(configuration ${CMAKE_CONFIGURATION_TYPES})
		# 对应于不同的构建版本
		set(dllpath "${depdir}/bin/${configuration}/${dllname}")
		if(EXISTS ${dllpath})
			configure_file(${dllpath} ${CMAKE_BINARY_DIR}/bin/${configuration}/${dllname} COPYONLY)
		endif()
	endforeach()
endmacro()

安装动态链接库,用于最终应用程序安装部署。

macro(install_dll depdir dllname)
	foreach(configuration ${CMAKE_CONFIGURATION_TYPES})
		set(dllpath "${depdir}/bin/${configuration}/${dllname}")
		if(EXISTS ${dllpath})
			install(FILES ${dllpath} DESTINATION bin/${configuration} CONFIGURATIONS ${configuration})
		endif()
	endforeach()
endmacro()

安装

设置安装路径前缀

使用install命令安装项目的时候,如果路径给出的是全路径,那么会直接使用这个路径,如果给出的是相对路径,那么会加上CMAKE_INSTALL_PREFIX,默认情况下这个值为C:/Program Files (x86)。可以直接在CMakeLists里面设置这个值,也可以在CMake GUI里面设置。

set(CMAKE_INSTALL_PREFIX "D:/Program Files (x86)/" CACHE PATH "Project install directory" FORCE)

根据配置设置安装目录

CMake预定义了四种编译配置: Debug、Release、RelWithDebInfo、MinSizeRel。大多数时候,我们只会安装Release版本。但是在某些情况下,比如A项目链接B项目的静态库,当A项目采用Debug编译时,需要链接B项目的Debug版本,当A项目采用Release编译时,需要链接B项目的Release版本。

假设我们使用了link_directories(${PROJECT_BINARY_DIR}/lib)命令设置了静态库查找目录,那么在Visual Studio中,实际查找目录是/projectpath/lib/projectpath/lib/$(Configuration)$(Configuration)表示编译配置。

因此,我们在设置安装目录时,也可以采用lib/Debuglib/Release这种结构。使用下面的代码可以完成这个功能。

foreach(lib_file platform)
	install(TARGETS ${lib_file}
		RUNTIME DESTINATION "bin/Debug" CONFIGURATIONS Debug
		LIBRARY DESTINATION "lib/Debug"  CONFIGURATIONS Debug
		ARCHIVE DESTINATION "lib/Debug" CONFIGURATIONS Debug
		)
	install(TARGETS ${lib_file}
		RUNTIME DESTINATION "bin/Release" CONFIGURATIONS Release
		LIBRARY DESTINATION "lib/Release"  CONFIGURATIONS Release
		ARCHIVE DESTINATION "lib/Release" CONFIGURATIONS Release
		)
	install(TARGETS ${lib_file}
		RUNTIME DESTINATION "bin/RelWithDebInfo" CONFIGURATIONS RelWithDebInfo
		LIBRARY DESTINATION "lib/RelWithDebInfo"  CONFIGURATIONS RelWithDebInfo
		ARCHIVE DESTINATION "lib/RelWithDebInfo" CONFIGURATIONS RelWithDebInfo
		)
	install(TARGETS ${lib_file}
		RUNTIME DESTINATION "bin/MinSizeRel" CONFIGURATIONS MinSizeRel
		LIBRARY DESTINATION "lib/MinSizeRel"  CONFIGURATIONS MinSizeRel
		ARCHIVE DESTINATION "lib/MinSizeRel" CONFIGURATIONS MinSizeRel
		)
endforeach()

工具

作为一个项目构建工具,CMake用一种优雅的方式,描述了项目构建的步骤。 但是对于大型项目而言,光是使用CMake简化项目构建过程还是不够的,还需要配备一套程序配合CMake来完成项目构建的过程。

编译加速

CMake只完成了从源文件到建立项目工程的过程,而真正编译,则还是要依赖于特定的编译工具,比如Visual Studio、gcc等。 对于大型工程而言,工程的编译时间也是一件值得优化的事。目前,有一些工具可以加速基于CMake构建的工程的编译过程。