1. 基础配置
1.1 设置项目版本和对应的头文件
一般来说,项目都有一个版本号,方便版本的发布,以及对问题进行溯源。
通过project
命令可以配置项目信息,如下:
project(CMakeTemplate VERSION 1.0.0 LANGUAGES C CXX)
第一个字段是项目名称;通过VERSION
指定版本号,格式为major.minor.patch.tweak
,并且CMake会将对应的值分别赋值给以下变量(如果没有设置,则为空字符串):
PROJECT_VERSION, <PROJECT-NAME>_VERSION
PROJECT_VERSION_MAJOR, <PROJECT-NAME>_VERSION_MAJOR
PROJECT_VERSION_MINOR, <PROJECT-NAME>_VERSION_MINOR
PROJECT_VERSION_PATCH, <PROJECT-NAME>_VERSION_PATCH
PROJECT_VERSION_TWEAK, <PROJECT-NAME>_VERSION_TWEAK
当结合configure_file
命令,可以配置自动生成版本头文件,将头文件版本号定义成对应的宏,或者定义成接口,方便在代码运行的时候了解当前的版本号。比如,在CMakeLists.txt中,加入以下字段:
configure_file(src/c/cmake_template_version.h.in "${PROJECT_SOURCE_DIR}/src/c/cmake_template_version.h")
然后,在src/c目录下添加cmake_template_version.h.in文件,内容如下:
#define CMAKE_TEMPLATE_VERSION_MAJOR @CMakeTemplate_VERSION_MAJOR@
#define CMAKE_TEMPLATE_VERSION_MINOR @CMakeTemplate_VERSION_MINOR@
#define CMAKE_TEMPLATE_VERSION_PATCH @CMakeTemplate_VERSION_PATCH@
当执行cmake
配置构建系统后,将会自动生成文件:cmake_template_version.h
,其中@<var-name>@
将会被替换为对应的值:
#define CMAKE_TEMPLATE_VERSION_MAJOR 1
#define CMAKE_TEMPLATE_VERSION_MINOR 0
#define CMAKE_TEMPLATE_VERSION_PATCH 0
1.2 指定编程语言版本
为了在不同机器上编译更加统一,最好用set
指令指定语言的版本,比如声明C使用c99
标准,C++使用c++11
标准:
set(CMAKE_C_STANDARD 99)
set(CMAKE_CXX_STANDARD 11)
Tips: CMAKE_
开头(包括project
命令自动设置的变量),这类变量都是CMake的内置变量,正是通过修改这些变量的值来配置CMake构建的行为。
1.3 配置编译选项
配置编译选项有两种方式:
-
add_compile_options
命令可以为所有编译器配置编译选项(同时对多个编译器生效)add_compile_options(-Wall -Wextra -pedantic -Werror)
-
变量
CMAKE_C_FLAGS
和CMAKE_CXX_FLAGS
可以单独配置C编译器和C++编译器的编译选项set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pipe -std=c99") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe -std=c++11")
1.4 配置编译类型
编译类型有四种,但是一般我们只用Release和Debug两种。如果我们不设置CMAKE_BUILD_TYPE
,即为空时,则默认是以Release方式进行编译。
设置编译类型:
-
变量
CMAKE_BUILD_TYPE
可以为所有编译器配置编译类型# 执行`cmake`命令的时候通过参数`-D`指定 cmake -B build -DCMAKE_BUILD_TYPE=Debug # CMakeLists.txt内部通过set指定,如果这条指令被执行,会把执行cmake时从外部传入的CMAKE_BUILD_TYPE的值覆盖掉 set(CMAKE_BUILD_TYPE Debug) # 如果希望外部传值不被覆盖掉,同时希望有一个默认值,则可以将CMAKE_BUILD_TYPE声明为缓存变量CACHE set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
我们可以针对不同的编译类型,单独为C编译器和C++编译器设置编译选项。
-
变量
CMAKE_[lang]_FLAGS_[DEBUG\RELEASE]
可以针对不同的编译器、不同的编译类型设置单独设置编译选项。# 设置编译Debug版本的C编译器选项 set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -O0") # 设置编译Debug版本的C++编译器选项 set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0") # 设置编译Debug版本的C编译器选项 set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O2") # 设置编译Debug版本的C++编译器选项 set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2")
Tips:默认情况下,
CMAKE_[lang]_FLAGS_DEBUG
会被设置为"-g"
选项,让编译器在生成可执行文件时生成调试信息,方便你在调试程序时查看变量的值、函数调用堆栈等信息。"-O0"
选项表示编译器不优化代码,方便你在调试时查看变量的真实值。对于Release
版本,不包含调试信息,故没有"-g"
选项,同时优化等级设置为"-O2"
。
Tips: 其中,还可以通过设置add_library()
的全局标志BUILD_SHARED_LIBS
来配置编译类型,如下所示:
set(BUILD_SHARED_LIBS ON) # default is OFF
add_library(${PROJECT_NAME} ${SRCS_MAIN})
ON
: 让 add_library() 生成 .dll 动态库,对应 SHAREDOFF
: 让 add_library() 生成 .lib 静态库,对应 STATIC ;默认值
1.5 添加全局宏定义
通常,我们需要通过不同的宏定义来实现不同的代码逻辑。通过命令add_definitions
可以添加全局的宏定义。具体用法如下:
-
在
CMakeLists.txt
中,使用add_definitions()
函数控制代码的开启和关闭option(TEST_DEBUG "option for debug" OFF) if (TEST_DEBUG) add_definitions(-DTEST_DEBUG) endif(TEST_DEBUG)
-
cmake
构建项目时可以添加参数控制宏的开启和关闭cmake -DTEST_DEBUG=1 .. #打开 cmake -DTEST_DEBUG=0 .. #关闭
-
在源码中通过条件句可以控制程序流向
#ifdef TEST_DEBUG ... ... #else ... #endif
1.6 添加include目录
源文件中有头文件,而我们需要让系统知道在哪里寻找相应的头文件。而通过命令include_directories
就可以来设置头文件的搜索目录。
include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])
Tips: 默认情况下,include_directories
命令会将目录添加到搜索列表的最后。但可以通过两种方法来修改添加的顺序:
-
通过命令设置
CMAKE_INCLUDE_DIRECTORIES_BEFORE
变量为ON
来将目录添加在列表前面include_directories(dir1 dir2) set(CMAKE_INCLUDE_DIRECTORIES_BEFORE ON) include_directories(dir3 dir4) # 此时dir3和dir4就会在dir1 dir2的前面
-
每次调用
include_directories
命令时使用AFTER
或BEFORE
选项来指定是添加到列表的前面或者后面。include_directories(dir1 dir2) set(CMAKE_INCLUDE_DIRECTORIES_BEFORE ON) include_directories(dir3 dir4) include_directories(AFTER dir5 dir6) # 列表顺序是<dir3 dir4 dir1 dir2 dir5 dir6>
如果使用SYSTEM
选项,会把指定目录当成系统的搜索目录。该命令作用范围只在当前的CMakeLists.txt。
1.7 向用户显示选项
在1.4节中提到,set指令的通常用法无法在外部通过cmake来修改变量的值,但可以设置为缓存变量,允许从外部修改。其实还有另一种更加直观且容易记忆的方法,推荐使用option()指令,允许外部设置逻辑变量的值。
具体操作如下:
1、用一个选项替换上一个示例的set(USE_LIBRARY OFF)
命令。该选项将修改USE_LIBRARY
的值,并设置其默认值为OFF
:
option(USE_LIBRARY "Compile sources into a library" OFF)
2、可以通过CMake的-D
CLI选项,将信息传递给CMake来切换库的行为:
$ mkdir -p build
$ cd build
$ cmake -D USE_LIBRARY=ON ..
-- ...
-- Compile sources into a library? ON
-- ...
-D
开关用于为CMake设置任何类型的变量:逻辑变量、路径等等。
工作原理
option
可接受三个参数:
option(<option_variable> "help string" [initial value])
<option_variable>
表示该选项的变量的名称。"help string"
记录选项的字符串,在CMake的终端或图形用户界面中可见。[initial value]
选项的默认值,可以是ON
或OFF
。
2. 编译目标文件(target)
一般来说,编译目标(target)
的类型一般有静态库、动态库和可执行文件
。 这时编写CMakeLists.txt
主要包括两步:
- 编译:确定编译目标所需要的源文件
- 链接:确定链接的时候需要依赖的额外的库
下面以开源项目(cmake-template)来演示。项目的目录结构如下:
./cmake-template
├── CMakeLists.txt
├── src
│ └── c
│ ├── cmake_template_version.h
│ ├── cmake_template_version.h.in
│ ├── main.c
│ └── math
│ ├── add.c
│ ├── add.h
│ ├── minus.c
│ └── minus.h
└── test
└── c
├── test_add.c
└── test_minus.
项目的构建任务为:
- 将math目录编译成静态库,命名为math
- 编译main.c为可执行文件demo,依赖math静态库
- 编译test目录下的测试程序,可以通过命令执行所有的测试(第4章)
- 支持通过命令将编译产物安装及打包(第3章)
2.1 编译库
将项目目录路径src/c/math
下的源文件编译为静态库,那么需要获取编译此静态库需要的文件列表,可以使用set
命令,或者file
命令来进行设置。
file(GLOB_RECURSE MATH_LIB_SRC
src/c/math/*.c
)
add_library(math STATIC ${MATH_LIB_SRC})
使用file
命令获取src/c/math
目录下所有的*.c
文件,然后通过add_library
命令编译名为math
的静态库,库的类型是第二个参数STATIC
指定的。
add_library
的用法如下:
add_library(<name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
[<source>...])
- 功能:将指定的源码编译到库中。
- 参数:
- name:目标名
- STATIC | SHARED | MODULE:常用的是静态库和动态库
- source:源文件
Tips:
静态库和动态库在不同的操作系统下的后缀名是不一样的,如下表所示:
可执行文件 | 静态库 | 动态库 | |
---|---|---|---|
Windows | .exe | .lib | .dll |
Linux | .a | .so |
2.2 编译可执行文件
通过add_executable
命令来往构建系统中添加一个可执行构建目标,同时指定编译需要的源文件。但是对于可执行文件来说,有时候还会依赖其他的库,则需要使用target_link_libraries
命令来声明构建此可执行文件需要链接的库。
Tips:target_link_libraries
相当于在vs中配置属性的链接器->输入->附加依赖项中添加lib这一操作。
在示例项目中,main.c
就使用了src/c/math
下实现的一些函数接口,所以依赖于前面构建的math
库。所以在CMakeLists.txt
中添加以下内容:
add_executable(demo src/c/main.c) # 可以添加多个源文件
target_link_libraries(demo math)
2.3 用条件句控制编译
我们通常希望能够在两种不同的编译方式之间进行切换,比如:
- 构建一个库,然后链接到可执行文件中
- 把所有源文件构建成一个可执行文件,但不生成任何一个库
此时就需要用到条件控制语句,下面给出一个示例:
1、首先,定义最低CMake版本、项目名称和支持的语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-04 LANGUAGES CXX)
2、我们引入了一个新变量USE_LIBRARY
,这是一个逻辑变量,值为OFF
。我们还打印了它的值:
set(USE_LIBRARY OFF)
message(STATUS "Compile sources into a library? ${USE_LIBRARY}")
3、CMake中定义BUILD_SHARED_LIBS
全局变量,并设置为OFF
。调用add_library
并省略第二个参数,将构建一个静态库:
set(BUILD_SHARED_LIBS OFF)
4、然后,引入一个变量_sources
,包括Message.hpp
和Message.cpp
:
list(APPEND _sources Message.hpp Message.cpp)
5、然后,引入一个基于USE_LIBRARY
值的if-else
语句。如果逻辑为真,则Message.hpp
和Message.cpp
将打包成一个库:
if(USE_LIBRARY)
# add_library will create a static library
# since BUILD_SHARED_LIBS is OFF
add_library(message ${_sources})
add_executable(hello-world hello-world.cpp)
target_link_libraries(hello-world message)
else()
add_executable(hello-world hello-world.cpp ${_sources})
endif()
2.4 设置库的属性
有时候,我们可能会生成同名的静态库和动态库,如果我们不进行设置的话,后面的同名库就无法生成。要避免这个问题,可以通过set_target_properties()
命令进行设置。
set_target_properties(target1 target2 ... PROPERTIES prop1 value1 prop2 value2 ...)
- 功能:设置目标文件的名称,对于动态库,还可以用来指定动态库版本和 API 版本。
假如我们想生成同名的动态库和静态库,如下所示:
add_library(hello SHARED ${LIBHELLO_SRC})
add_library(hello STATIC ${LIBHELLO_SRC})
但是这样并不会创建静态库,因为在cmake中,target是不能重名
的,所以生成静态库的指令是无效的。
要解决这个问题,我们需要用到set_target_properties
命令,具体的CMakeList.txt为:
add_library(hello SHARED ${LIBHELLO_SRC})
add_library(hello_static STATIC ${LIBHELLO_SRC}) # 先修改名字
SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello") # 然后再修改名字,此时如果没有后面的命令,则生成的静态库会把动态库覆盖掉,因为依然是存在同名
SET_TARGET_PROPERTIES(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1) # 不清除同名库
SET_TARGET_PROPERTIES(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)
除了生成同名库的作用,set_target_properties
还可以指定动态库版本和 API 版本,比如:
set_target_properties(hello PROPERTIES VERSION 1.2 SOVERSION 1)
- 参数:
VERSION
指代动态库版本SOVERSION
指代 API 版本
此时生成的动态库有:
- libhello.so.1.2:动态库的文件名
- libhello.so ->libhello.so.1:动态库的链接名
- libhello.so.1->libhello.so.1.2:动态库的别名
3. 安装和打包
3.1 安装
我们在版本发布给客户的时候,需要把库、可执行文件和头文件输出到一个文件夹中,这种过程就叫做安装。安装一般分为两个步骤:
- 通过
install
命令来说明需要安装的内容及目标路径; - 通过设置
CMAKE_INSTALL_PREFIX
变量说明安装的路径或者可以使用cmake --install --prefix <install-path>
覆盖指定安装路径(3.15
往后的版本)
比如,在示例项目中,把math
和demo
两个目标按文件类型安装:
install(TARGETS math demo
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
这里通过TARGETS
参数指定需要安装的目标列表;参数RUNTIME DESTINATION
、LIBRARY DESTINATION
、ARCHIVE DESTINATION
分别指定可执行文件、库文件、归档文件分别应该安装到安装目录下个哪个子目录。
如果指定CMAKE_INSTALL_PREFIX
为/usr/local
,那么math
库将会被安装到路径/usr/local/lib/
目录下;而demo
可执行文件则在/usr/local/bin
目录下。
Tips:CMAKE_INSTALL_PREFIX
在不同的系统上有不同的默认值,使用的时候最好显式指定路径。Unix系统的默认值为 /usr/local
, Windows的默认值为 c:/Program Files/${PROJECT_NAME}
。
同时,还可以使用install
命令安装头文件:
file(GLOB_RECURSE MATH_LIB_HEADERS src/c/math/*.h)
install(FILES ${MATH_LIB_HEADERS} DESTINATION include/math)
3.2 打包
要使用打包功能,需要执行include(CPack)
启用相关的功能,在执行构建编译之后使用cpack
命令行工具进行打包安装。
打包的内容就是install
命令安装的内容,关键需要设置的变量有:
变量 | 描述 |
---|---|
CPACK_GENERATOR | 打包使用的压缩工具,比如"ZIP" |
CPACK_OUTPUT_FILE_PREFIX | 打包安装的路径前缀 |
CPACK_INSTALL_PREFIX | 打包压缩包的内部目录前缀 |
CPACK_PACKAGE_FILE_NAME | 打包压缩包的名称,打包压缩包的名称,由CPACK_PACKAGE_NAME、CPACK_PACKAGE_VERSION、CPACK_SYSTEM_NAME三部分构成 |
比如:
include(CPack)
set(CPACK_GENERATOR "ZIP")
set(CPACK_PACKAGE_NAME "CMakeTemplate")
set(CPACK_SET_DESTDIR ON)
set(CPACK_INSTALL_PREFIX "")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
假如: CPACK_OUTPUT_FILE_PREFIX
设置为/usr/local/package
; CPACK_INSTALL_PREFIX
设置为real/cool/engineer
; CPACK_PACKAGE_FILE_NAME
设置为CMakeTemplate-1.0.0
; 那么执行打包文件的生成路径为:
/usr/local/package/CMakeTemplate-1.0.0.zip
解压这个包得到的目标文件则会位于路径下:
/usr/local/package/real/cool/engineer/
此时重新执行构建,使用cpack
命令执行打包:
$ cmake -DCPACK_OUTPUT_FILE_PREFIX=`pwd`/output ..
$ cmake --build .
$ cpack
4. 测试
因为笔者目前还不需要用到测试,所以先不加这部分了。感兴趣的可以看参考链接里面的内容。
5. 其他
5.1 常用的宏定义
编译器默认是使用ASCII对字符串编码的,但是当我们在程序中需要用到中文时,就需要用到下面两个宏定义,其作用是告诉编译器,当字符串是英文时,使用ASCII编码,当字符串是中文时,使用UNICODE编码。至于为什么需要两个功能一样的宏定义,那是因为为了适配不同的平台。
if(MSVC)
add_definitions(-DUNICODE)
add_definitions(-D_UNICODE)
endif()
5.2 警告设置
if(MSVC)
if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]" OR CMAKE_C_FLAGS MATCHES "/W[0-4]")
string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
else()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W4")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4")
endif()
ADD_DEFINITIONS(
-D_CRT_SECURE_NO_WARNINGS
-D_CRT_SECURE_NO_DEPRECATE
)
elseif((";${gcc_like_compilers};" MATCHES ";${CMAKE_CXX_COMPILER_ID};"))
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Werror")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror")
endif()
5.3 OpenMP
find_package(OpenMP)
if (OPENMP_FOUND)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}")
endif (OPENMP_FOUND)
参考
还有参考其他参考的网页,太多了,就只列举参考最多的网页了。