Cmake 应用实践

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_FLAGSCMAKE_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 动态库,对应 SHARED
  • OFF: 让 add_library() 生成 .lib 静态库,对应 STATIC ;默认值

1.5 添加全局宏定义

通常,我们需要通过不同的宏定义来实现不同的代码逻辑。通过命令add_definitions可以添加全局的宏定义。具体用法如下:

  1. CMakeLists.txt中,使用add_definitions()函数控制代码的开启和关闭

    option(TEST_DEBUG "option for debug" OFF)
    if (TEST_DEBUG) 
    	add_definitions(-DTEST_DEBUG)
    endif(TEST_DEBUG)
    
  2. cmake构建项目时可以添加参数控制宏的开启和关闭

    cmake -DTEST_DEBUG=1 .. #打开
    cmake -DTEST_DEBUG=0 .. #关闭
    
  3. 在源码中通过条件句可以控制程序流向

    #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命令时使用AFTERBEFORE选项来指定是添加到列表的前面或者后面。

    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的-DCLI选项,将信息传递给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]选项的默认值,可以是ONOFF

2. 编译目标文件(target)

一般来说,编译目标(target)的类型一般有静态库、动态库和可执行文件。 这时编写CMakeLists.txt主要包括两步:

  1. 编译:确定编译目标所需要的源文件
  2. 链接:确定链接的时候需要依赖的额外的库

下面以开源项目(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.

项目的构建任务为:

  1. 将math目录编译成静态库,命名为math
  2. 编译main.c为可执行文件demo,依赖math静态库
  3. 编译test目录下的测试程序,可以通过命令执行所有的测试(第4章)
  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.hppMessage.cpp

list(APPEND _sources Message.hpp Message.cpp)

5、然后,引入一个基于USE_LIBRARY值的if-else语句。如果逻辑为真,则Message.hppMessage.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 版本

此时生成的动态库有:

  1. libhello.so.1.2:动态库的文件名
  2. libhello.so ->libhello.so.1:动态库的链接名
  3. libhello.so.1->libhello.so.1.2:动态库的别名

3. 安装和打包

3.1 安装

我们在版本发布给客户的时候,需要把库、可执行文件和头文件输出到一个文件夹中,这种过程就叫做安装。安装一般分为两个步骤:

  1. 通过install命令来说明需要安装的内容及目标路径;
  2. 通过设置CMAKE_INSTALL_PREFIX变量说明安装的路径或者可以使用cmake --install --prefix <install-path>覆盖指定安装路径(3.15往后的版本)

比如,在示例项目中,把mathdemo两个目标按文件类型安装:

install(TARGETS math demo
        RUNTIME DESTINATION bin
        LIBRARY DESTINATION lib
        ARCHIVE DESTINATION lib)

这里通过TARGETS参数指定需要安装的目标列表;参数RUNTIME DESTINATIONLIBRARY DESTINATIONARCHIVE 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/packageCPACK_INSTALL_PREFIX设置为real/cool/engineerCPACK_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)

参考

CMake实践应用专题

还有参考其他参考的网页,太多了,就只列举参考最多的网页了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值