CMake使用入门教程

近几年美国对中国的科技封杀十分严重,说不定哪天Windows操作系统就不让我们用了。因此,在构建C/C++项目时应考虑跨平台,哪天不让用Windows了,可以请容易的将项目移植到Linux上。不同系统平台有不同的C/C++编译器,不同编译器有不同的构建规则,针对每个平台的不同编译器编写构建规则十分复杂,幸好有CMake可简化构建规则的编写,实现一次编写,不同平台适用。下面简单介绍CMake的使用。

CMake简介

代码变成可执行文件,叫做编译(compile);先编译这个,还是先编译那个(即编译的安排),叫做构建(build)。

Make是最常用的构建工具,诞生于1977年,主要用于C语言的项目。但是实际上 ,任何只要某个文件有变化,就要重新构建的项目,都可以用Make构建。

Make工具有很多,例如 GNU Make ,QT 的 qmake ,微软的 MS nmake,BSD Make(pmake),Makepp,等等。这些 Make 工具遵循着不同的规范和标准,所执行的 Makefile 格式也千差万别。这样就带来了一个严峻的问题:如果软件想跨平台,必须要保证能够在不同平台编译。而如果使用上面的 Make 工具,就得为每一种标准写一次 Makefile ,这将是一件让人抓狂的工作。

CMake就是针对上面问题所设计的工具:它首先允许开发者编写一种平台无关的 CMakeList.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 和工程文件,如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程。从而做到“Write once, run everywhere”。显然,CMake 是一个比上述几种 make 更高级的编译配置工具。一些使用 CMake 作为项目架构系统的知名开源项目有 VTK、ITK、KDE、OpenCV、OSG 等。

CMake教程入门

给工程起个名字

语法:

1
project(<PROJECT-NAME> [LANGUAGES] [<language-name>...])

该指令定义工程名称。例如:

1
project(UtilTool)

添加头文件目录INCLUDE_DIRECTORIES

语法:

1
include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])

它相当于g++选项中的-I参数的作用,也相当于环境变量中增加路径到CPLUS_INCLUDE_PATH变量的作用。例如:

1
include_directories(${CMAKE_CURRENT_LIST_DIR}/include)

当头文件分散在不同层次和深度的目录中时,逐个使用include_directories命令添加包含目录十分麻烦,可使用如下方法递归加载各个目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
MARO(HEADER_DIRECTORIES return_list)
FILE(GLOB_RECURSE new_list src/*.h)
SET(dir_list "")
FOREACH(file_path ${new_list})
GET_FILENAME_COMPONENT(dir_path ${file_path} PATH)
SET(dir_list ${dir_list} ${dir_path})
ENDFOREACH()
LIST(REMOVE_DUPLICATES dir_list)
SET(${return_list} ${dir_list})
ENDMACRO()

HEADER_DIRECTORIES(header_dir_list)
include_directories(${CMAKE_CURRENT_LIST_DIR}/include ${header_dir_list})

语法:

1
link_directories(directory1 directory2 ...)

它相当于g++命令的-L选项的作用,也相当于环境变量中增加LD_LIBRARY_PATH的路径的作用。

1
link_directories(${CMAKE_CURRENT_LIST_DIR}/lib)

向当前工程添加存放源文件的子目录ADD_SUBDIRECTORY

ADD_SUBDIRECTORY用于向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制存放的位置。语法如下:

1
2
ADD_SUBDIRECTORY(source_dir [binary_dir]
[EXCLUDE_FROM_ALL])

上面的例子定义了将 src 子目录加入工程,并指定了编译输出路径为 bin 目录。如果不指定 bin 目录,那么编译的结果都将存放在 build/src 目录。

语法:

1
2
target_link_libraries(<target> [item1 [item2 [...]]]
[[debug|optimized|general] <item>] ...)

该指令的作用为将目标文件与库文件进行链接。例如:

1
TARGET_LINK_LIBRARIES(utiltool-example utiltool)

为工程生成目标文件

语法:

1
2
3
add_executable(<name> [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL]
source1 [source2 ...])

例如:

1
ADD_EXECUTABLE(utiltool-example examples/ConverterTest.cpp)

为工程生成共享库

语法:

1
2
3
add_library(<name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
[source1] [source2] [...])

该指令的主要作用就是将指定的源文件生成链接文件,然后添加到工程中去。例如:

1
ADD_LIBRARY(utiltool SHARED ${UTILTOOL_SOURCES})

为工程制作简单的安装脚本

语法:

1
2
3
4
5
6
7
8
9
install(TARGETS targets... [EXPORT <export-name>]
[[ARCHIVE|LIBRARY|RUNTIME|FRAMEWORK|BUNDLE|
PRIVATE_HEADER|PUBLIC_HEADER|RESOURCE]
[DESTINATION <dir>]
[PERMISSIONS permissions...]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[OPTIONAL] [NAMELINK_ONLY|NAMELINK_SKIP]
] [...])

该命令为一个工程生成安装规则。TARGETS格式的install命令规定了安装工程中的目标(targets)的规则。有5中可以被安装的目标文件:ARCHIVE,LIBRARY,RUNTIME,FRAMEWORK,和BUNDLE。静态链接的库文件总是被当做ARCHIVE目标。模块库总是被当做LIBRARY目标。例如:

1
2
3
4
5
install(TARGETS utiltool 
RUNTIME DESTINATION bin
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib)
install(FILES ${UTILTOOL_HEADERS} DESTINATION include)

为工程设置变量

语法:

1
2
set(<variable> <value>
[[CACHE <type> <docstring> [FORCE]] | PARENT_SCOPE])

该指令用于给一般变量,缓存变量,环境变量赋值。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
# set up versioning.
set(BUILD_MAJOR "1")
set(BUILD_MINOR "0")
set(BUILD_VERSION "0")
set(BUILD_VERSION ${BUILD_MAJOR}.${BUILD_MINOR}.${BUILD_VERSION})

SET(UTILTOOL_HEADERS
src/GeodeticConverter.h
)

SET(UTILTOOL_SOURCES
src/GeodeticConverter.cpp
)

为工程设置预定义宏

语法:

1
add_definitions(-DFOO -DBAR ...)

该指令添加编译参数。例如:

1
2
# 添加WIN32宏定义
add_definitions(-DWIN32)

OPTION变量

语法:

1
2
option(<option_variable> "help string describing option"
[initial value])

该指令提供一个用户可以任选的选项,可在之后由用户通过CMake的GUI或者命令行进行更改。例如:

1
OPTION(UTILTOOL_EXAMPLES "Build the examples" ON)

修改默认的CMAKE_MODULE_PATH目录

CMAKE_MODULE_PATH是供find_package搜索第三方库用的。cmake的默认Modules目录在安装目录中:cmake-3.11.3-win64-x64\share\cmake-3.11\Modules。
如果要追加Modules目录,有3种方式:

1
2
3
4
5
SET(CMAKE_MODULE_PATH "${OpenSceneGraph_SOURCE_DIR}/CMakeModules;${CMAKE_MODULE_PATH}")

LIST(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_PREFIX}")

set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake)

find_package用法

通常情况下,包含第三方库需要写以下内容:

1
2
3
4
include_directories("${project_root_path}/include/")  
link_directories(./lib)
add_executable(myapp myapp.cpp)
target_link_libraries(myapp mylib)

如果引用的很多个第三方库,那么类似上面的内容会写很多,且如果自己的多个项目都引用了某个第三方库,那么我每个项目的CmakeList.txt都得写一遍,重复劳动很多。那么有没办法为每个第三方库只定义一次它的头文件和库文件信息,然后在自己的工程中只指定名称即可?(类似编译Java的Maven仓库)答案是当然可以,find_package帮你解决。

find_package定义在自己工程的CmakeList.txt中:

1
find_package( XXX CONFIG REQUIRED )

然后cmake就会在默认的Modules(即CMAKE_MODULE_PATH指定的目录)目录中搜索这个XXX第三方库。

搜索有两种模式:FindXXX.cmake和XXXConfig.cmake。前者叫做Module模式,后者叫做Config模式。

优雅的软件项目结构模板

请参考CMake—优雅地构建软件项目实践(1)

完整简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# CMakeList.txt: UtilTool 的 CMake 项目,在此处包括源代码并定义
# 项目特定的逻辑。
#
cmake_minimum_required (VERSION 3.8)

project(UtilTool)

# set up versioning.
set(BUILD_MAJOR "1")
set(BUILD_MINOR "0")
set(BUILD_VERSION "0")
set(BUILD_VERSION ${BUILD_MAJOR}.${BUILD_MINOR}.${BUILD_VERSION})

include_directories(${CMAKE_CURRENT_LIST_DIR}/include)

link_directories(${CMAKE_CURRENT_LIST_DIR}/lib)

link_libraries(gsl)

# OS and compiler checks.
if(UNIX)
# linux / normal unix
add_definitions(-D_LINUX)
if(CYGWIN)
# Special Cygwin stuff here
elseif(APPLE)
# Special Apple stuff here
remove_definitions(-D_LINUX)
add_definitions(-D_DARWIN)
endif()
elseif(WIN32)
add_definitions(-DWIN32)
add_definitions(-D UtilTool_EXPORTS)
if(MINGW)
# Special MINGW stuff here
elseif(MSVC)
# Special MSVC stuff here
add_definitions(-D_WINSOCK_DEPRECATED_NO_WARNINGS)
else()
# No idea what it is, but there's a good chance it's too weird.
MESSAGE( FATAL_ERROR "Using unknown WIN32 compiler... NOT. Please add to build system." )
endif()
endif()

SET(UTILTOOL_HEADERS
src/GeodeticConverter.h
)

SET(UTILTOOL_SOURCES
src/GeodeticConverter.cpp
)

# mark headers as headers...
SET_SOURCE_FILES_PROPERTIES( ${UTILTOOL_HEADERS} PROPERTIES HEADER_FILE_ONLY TRUE )
# append to sources so that dependency checks work on headers
LIST(APPEND UTILTOOL_SOURCES ${UTILTOOL_HEADERS})

OPTION(UTILTOOL_SHARED "Build utiltool lib as shared." ON)
OPTION(UTILTOOL_DEP_ONLY "Build for use inside other CMake projects as dependency." OFF)

# 将源代码添加到此项目的共享库中。
if(UTILTOOL_SHARED)
ADD_LIBRARY(utiltool SHARED ${UTILTOOL_SOURCES})
endif()

# install into configured prefix
if(NOT UTILTOOL_DEP_ONLY)
install(TARGETS utiltool
RUNTIME DESTINATION bin
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib)
install(FILES ${UTILTOOL_HEADERS} DESTINATION include)
else()

endif()

# TODO: 如有需要,请添加测试并安装目标。
OPTION(UTILTOOL_EXAMPLES "Build the examples" ON)

if(UTILTOOL_EXAMPLES)
ADD_EXECUTABLE(utiltool-example examples/ConverterTest.cpp)
TARGET_LINK_LIBRARIES(utiltool-example utiltool)
endif()

参考链接

  1. Linux平台编译安装测试JSBSim,by jackhuang.
  2. 干货:构建C/C++良好的工程结构,by Froser.
  3. 基于CMake构建系统的C++工程框架,by zhongxiao_yao.
  4. CMake 手册详解(十九)install指令,by SirDigit.
  5. CMake中变量总结,by 拾荒志.
  6. cmake使用教程(十)-关于file,by saka.
  7. Recursive CMake search for header and source files,by stackoverflow.
  8. CMake shared library in subdirectory,by stackoverflow.
  9. CMake 语言和语法,by leosocy.
  10. 【cmake】——include_directories 和target_include_directories的区别,by 大川搬砖.
  11. CMake—优雅地构建软件项目实践(1),by Ethan.
  12. CMake测试,by dxa572862121.
  13. CMake–Set用法,by narjaja.
  14. CMake之message()函数的使用和打印变量值,by hp_cpp.
  15. A minimal CMake project template,by Matt Morse.
  16. [Build]cmake常用配置项,by 玄冬Wong.
  17. 简单介绍Cmake生成VS工程中的ALL_BUILD、INSTALL、ZERO_CHECK作用!!,by 醉逍遥_祥.