CMake使用教程

Posted crazyang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CMake使用教程相关的知识,希望对你有一定的参考价值。

为什么需要CMake

如果你一直在windows平台上开发,使用最多的可能就是VS的开发环境,它已经集成了全套的开发环境包括构建编译等。你或许听过好几种 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 作为项目架构系统的知名开源项目有 VTKITKKDEOpenCVOSG 等。

Linux 下安装CMake

1、安装gcc等必备程序包(已安装则略过此步)

yum install -y gcc gcc-c++ make automake 

2、安装wget (已安装则略过此步)

yum install -y wget

3、安装OpenSSL

yum install openssl
yum install openssl-devel

4、获取CMake源码包

wget https://cmake.org/files/v3.19/cmake-3.19.0-rc1.tar.gz

5、解压CMake源码包

tar -zxvf cmake-3.19.0-rc1.tar.gz

6、进入cmark的源码目录

cd cmake-3.19.0-rc1

7、运行当前目录下的一个文件

./bootstrap

8、运行gmake命令(这步时间有点长),如果安装了make却提示找不到gmake,只需要使用sudo ln -s /usr/bin/make /usr/bin/gmake,然后

gmake

9、进行安装

sudo gmake install

10、 安装完成,查看cmake版本号,如果输出版本号,则安装成功。

cmake --version

入门:单个源文件

简单的项目,只需要单个源文件就可以。如果现在有一个main.cpp,内容如下:

#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
    cout << "Hello CMake!" << endl;
    return 0;
}

我们在同级目录下新建一个CMakelist.txt,其内容如下:

# CMake 最低版本号要求
cmake_minimum_required(VERSION 3.19)
# 项目名称
project(demo1)
# 指定生成目标,将main.cpp生成一个名为demo1的可以执行文件
add_executable(demo1 main.cpp)

需要注意的是,CMake是不区分大小写的;# 开头该行会被视为注释;命令由命令名称、小括号、参数组成;参数之间用空格间隔。

CMake编译运行

现在我们在当前目录中使用cmake .执行,可以观察到多了几个和CMake相关的文件,如下:

CMakeCache.txt  CMakeFiles  CMakelists.txt  Makefile  cmake_install.cmake  demo1  main.cpp

其中有一个文件是Makefile,再用make命令编译就得到demo1可执行文件。

  • 直接生成二进制文件

还可以直接调用CMake构建系统以实际编译/链接项目

cmake --build .

可以看到生成了可执行二进制文件。

同目录多个源文件

实际项目中在一个目录中会有很多个源文件,现在开始编写一个目录中,多个源文件情况:

├── CMakelists.txt
├── MyMath.cpp
├── MyMath.h
└── main.cpp

MyMath.h文件内容如下:

#pragma once
int my_add(int a, int b);

MyMath.cpp文件内容如下:

#include "MyMath.h"
int my_add(int a, int b)
{
    return a + b;
}

main.cpp文件内容如下:

#include <iostream>
#include "MyMath.h"
using namespace std;
int main(int argc, char **argv)
{
    cout << my_add(1, 2) << endl;
    return 0;
}

CMakelists.txt文件内容如下:

# CMake 最低版本号要求
cmake_minimum_required(VERSION 3.19)
# 项目名称
project(demo2)
# 指定生成目标,将main.cpp,MyMath.cpp生成一个名为demo2的可以执行文件
add_executable(demo2 main.cpp MyMath.cpp)

和单个文件类似,仅仅只在add_executable中添加了一个MyMath.cpp源文件,虽然这样写没有问题,但是当源文件很多时,把所有的源文件加入是很繁琐的工作。

我们可以使用aux_source_directory命令,该命令会查找指定目录下所有的源文件,并将结果存在指定的变量名中,如下:

aux_source_directory(<dir> <variable>)

因此,修改CMakeList为如下:

# CMake 最低版本号要求
cmake_minimum_required(VERSION 3.19)
# 项目名称
project(demo2)
# 将当前目录所有源文件保存在变量DIR_SRCS中
aux_source_directory(. DIR_SRCS)
# 指定生成目标,将DIR_SRCS变量的所有源文件生成一个名为demo2的可以执行文件
add_executable(demo2 ${DIR_SRCS})

多目录&多源文件

现在我们的项目中不仅有多个源文件,还有一些其他的库目录,比如我们要使用一个自己的math目录,其文件结构如下:

├── CMakelists.txt
├── MyPrint.cpp
├── MyPrint.h
├── main.cpp
└── math
    ├── CMakelists.txt
    ├── MyMath.cpp
    └── MyMath.h
  • 子目录math下文件内容

MyPrint.h的文件内容:

#pragma once
int my_add(int a, int b);

MyPrint.cpp的文件内容:

#include "MyMath.h"
int my_add(int a, int b)
{
    return a + b;
}

CMakelists.txt的文件内容:

# 将当前目录所有源文件保存在变量DIR_LIB_SRCS中
aux_source_directory(. DIR_LIB_SRCS)
# 将DIR_LIB_SRCS变量中的所有源文件编译成静态链接库,链接库名为MyMath
add_library (MyMath ${DIR_LIB_SRCS})
  • 主目录下文件内容

MyPrint.h的文件内容:

#pragma once
void my_print(int a);

MyPrint.cpp的文件内容:

#include "MyPrint.h"
#include <stdio.h>
void my_print(int a)
{
    printf("%d", a);
}

main.cpp的文件内容:

#include "math/MyMath.h"
#include "MyPrint.h"
#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
    my_print(my_add(1, 2));
    return 0;
}

CMakelists.txt的文件内容:

# CMake 最低版本号要求
cmake_minimum_required(VERSION 3.19)
# 项目名称
project(demo3)
# 将当前目录所有源文件保存在变量DIR_SRCS中
aux_source_directory(. DIR_SRCS)
# 添加子目录
add_subdirectory(math)
# 指定生成目标,将DIR_SRCS变量的所有源文件生成一个名为demo2的可以执行文件
add_executable(demo3 ${DIR_SRCS})
# 添加链接库,MyMath是子目录中的链接库名
target_link_libraries(demo3 MyMath)

add_subdirectory指明本项目包含一个子目录math,这样math下的CMakelists.txt文件和源代码也会被处理。

target_link_libraries指明可执行文件 demo3需要链接一个名为MyMath的链接库。

add_library将某个目录中的源文件编译为静态链接库。

自定义编译选项

CMake可以为项目增加编译选项,从而可以根据用户的环境和需求选择最合适的编译方案。

例如,可以将MyMath库设计为一个可选库,如果该选项为ON,就是用该库中定义的数学函数来进行宏运算。否则就调用标准库中的数学函数库。

现在用abs函数来举例,在MyMath库中重新定义一个my_abs函数,当前文件目录如下:

├── CMakelists.txt
├── config.h.in
├── main.cpp
└── math
    ├── CMakelists.txt
    ├── MyMath.cpp
    └── MyMath.h
  • 子目录math下文件内容

MyMath.h的文件内容:

#pragma once
int my_abs(int a);

MyMath.cpp的文件内容:

#include "MyMath.h"
int my_abs(int a)
{
    return a >= 0 ? a : -a;
}

CMakelists.txt的文件内容:

# 将当前目录所有源文件保存在变量DIR_LIB_SRCS中
aux_source_directory(. DIR_LIB_SRCS)
# 生成链接库名为MyMath
add_library (MyMath ${DIR_LIB_SRCS})
  • 主目录下文件内容

config.h.in的文件内容:

#cmakedefine USE_MYMATH

main.cpp的文件内容:

#include "math/MyMath.h"
#include "config.h"
#include <stdio.h>

#ifdef USE_MYMATH
#include "math/MyMath.h"
#else
#include "math.h"
#endif

int main(int argc, char **argv)
{
#ifdef USE_MYMATH
    printf("my abs: %d", my_abs(-1));
#else
    printf("std abs: %d", abs(-1));
#endif
    return 0;
}

CMakelists.txt的文件内容:

# CMake 最低版本号要求
cmake_minimum_required(VERSION 3.19)
# 项目名称
project(demo4)

# 加入一个配置头文件,用于处理CMake对源码的设置
configure_file(
    "${PROJECT_SOURCE_DIR}/config.h.in"
    "${PROJECT_BINARY_DIR}/config.h"
)
# 是否使用自己的MyMath库
option(USE_MYMATH "Use MyMath" ON)
# 是否加入MyMath库
if(USE_MYMATH)
    include_directories ("${PROJECT_SOURCE_DIR}/math")
    add_subdirectory (math)  
    list(APPEND EXTRA_LIBS MyMath)
    list(APPEND EXTRA_INCLUDE "${PROJECT_SOURCE_DIR}/math")
endif(USE_MYMATH)

# 将当前目录所有源文件保存在变量DIR_SRCS中
aux_source_directory(. DIR_SRCS)
# 指定生成目标,将DIR_SRCS变量的所有源文件生成一个名为demo2的可以执行文件
add_executable(demo4 ${DIR_SRCS})

# 添加链接库,MyMath是子目录中的库的名字
target_link_libraries(demo4 PUBLIC ${EXTRA_LIBS})
# 添加二进制树到头文件搜索路径,才能发现config.h
target_include_directories(demo4 PUBLIC "${PROJECT_BINARY_DIR}" ${EXTRA_INCLUDE})

其中configure_file命令将config.h.in生成config.h文件,通过这种机制,可以通过预定义一些参数和变量来控制代码的生成。

option命令添加了一个USE_MYMATH选项,并且设置为ON

根据 USE_MYMATH 变量的值来决定是否使用我们自己编写的MyMath库。

main.cpp中,主函数根据USE_MYMATH的预定义值决定使用标准库还是MyMath库。

主函数中引入头文件#include "config.h",我们并不直接编写这个文件,而是从CMakeList.txt中配置的config.h.in文件来生成它,我们只需要编写config.h.in文件,其内容如上面代码所示,如果option的USE_MYMATH为ON,生成的config.h中定义的内容是#define USE_MYMATH,如果为OFF,生成的config.h中定义的内容是/* #undef USE_MYMATH */

还可以交互式的选择该变量的值,使用ccmake命令,也可以使用 cmake -i 命令,该命令会提供一个会话式的交互式配置界面,从中可以找到定义的 USE_MYMATH 选项,按键盘的方向键可以在不同的选项窗口间跳转,按下 enter 键可以修改该选项。修改完成后可以按下 c 选项完成配置,之后再按 g 键确认生成 Makefile 。ccmake 的其他操作可以参考其窗口下方给出的指令提示。

定制安装规则&测试

CMake可以指定安装规则,以及添加测试。这两个功能分别可以通过在产生Makefile后使用make install来执行。在以前的GNU Makefile中,你可能需要为此编写installtest两个伪目标和相应的规则,但是在CMake里,这样的工作同样只需要几条命令。

为工程定制安装规则

还是使用上面demo4例子,首先在math/CMakelists.txt文件末尾添加下面几行:

# 指定MyMath库的安装路径
install(TARGETS MyMath DESTINATION bin)
install(FILES MyMath.h DESTINATION include)

用来指明MyMath库的安装路径。然后再修改根目录的CMakelists.txt文件,在末尾添加下面几行:

# 指定安装路径
install(TARGETS demo4 DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/config.h" DESTINATION include)

通过上卖弄的定制,生成的Demo文件和MyMath函数库libMyMath.a文件会被复制到/usr/local/bin 中,而MyMath.h和生成的config.h文件则会被复制到/usr/local/include中。我们可以验证一下(注意:这里的/usr/local/是默认安装到的根目录,可以通过修改CMAKE_INSTALL_PREFIX变量的值来指定这些文件应该拷贝到哪个根目录)

[crazyang@zt demo4]$ sudo make install
[ 50%] Built target MyMath
[100%] Built target demo4
Install the project...
-- Install configuration: ""
-- Installing: /usr/local/bin/libMyMath.a
-- Installing: /usr/local/include/MyMath.h
-- Installing: /usr/local/bin/demo4
-- Installing: /usr/local/include/config.h
[crazyang@zt demo4]$ ls /usr/local/bin/
demo4  libMyMath.a
[crazyang@zt demo4]$ ls /usr/local/include/
MyMath.h  config.h

为工程添加测试

添加测试也比较简单。CMake提供了一个称为CTest的测试工具。我们要做的只是在项目根目录的CMakelists文件中调用一系列的add_test命令。

还是使用demo4举例子,但是为了方便演示,将demo4的主函数修改一下,使用传入的参数:

#include "math/MyMath.h"
#include "config.h"
#include <stdio.h>
#include <stdlib.h>

#ifdef USE_MYMATH
#include "math/MyMath.h"
#else
#include "math.h"
#endif

int main(int argc, char **argv)
{
    if (argc < 2)
    {
        printf(Usage:);
        return 1;
    }
#ifdef USE_MYMATH
    printf("my abs: %d", my_abs(atoi(argv[1])));
#else
    printf("std abs: %d", abs(atoi(argv[1])));
#endif
    return 0;
}

在根目录的CMakelists.txt文件,在末尾添加下面几行:

# 启用测试
enable_testing()

# 测试程序是否成功运行
add_test(test_run demo4 -1)
# 测试帮助信息是否可以正常提示
add_test (test_usage demo4)
set_tests_properties (test_usage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*")

# 测试-2的绝对值
add_test(test_-2 demo4 -2)
set_tests_properties (test_-2 PROPERTIES PASS_REGULAR_EXPRESSION "2")
# 测试5的绝对值
add_test(test_5 demo4 5)
set_tests_properties (test_5 PROPERTIES PASS_REGULAR_EXPRESSION "5")

上面的CMake包含了3个测试,test_run用来测试程序是否成功并返回0值,括号中分别是测试名,二进制运行文件名,主函数传入的参数。剩下的两个测试分别用来测试-2的绝对值、5的绝对值是否能得到正确的结果。其中PASS_REGULAR_EXPRESSION用来测试输出是否包含后面跟着的字符串。

测试的结果如下:

[crazyang@zt-2013412:/mnt/d/CODE/demo4]$ make test
Running tests...
Test project /mnt/d/CODE/demo4
    Start 1: test_run
1/4 Test #1: test_run .........................   Passed    0.04 sec
    Start 2: test_usage
2/4 Test #2: test_usage .......................   Passed    0.05 sec
    Start 3: test_1
3/4 Test #3: test_1 ...........................   Passed    0.04 sec
    Start 4: test_2
4/4 Test #4: test_2 ...........................   Passed    0.04 sec

100% tests passed, 0 tests failed out of 4

Total Test time (real) =   0.17 sec

如果测试太多数据,按上面那么写会非常繁琐。这是可以通过编写宏来实现:

# 定义一个宏,用来简化测试工作
macro(do_test arg1 result)
    add_test(test_${arg1} demo4 ${arg1})
    set_tests_properties(test_${arg1} PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endmacro(do_test arg1 result)

# 使用该宏进行一系列的数据测试
do_test(-1 "1")
do_test(-5 "5")
do_test(10 "10")

关于CTest的更详细文档可以用过命令man 1 ctest 参考CTest的文档。

支持gdb调试

使CMake支持gdb设置也很容易,只需要指定Debug模式下开启-g选项:

set(CMAKE_BUILD_TYPE "Debug")
set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb")
set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")

之后可以直接对生成的程序使用 gdb 来调试。

以上是关于CMake使用教程的主要内容,如果未能解决你的问题,请参考以下文章

VIM 代码片段插件 ultisnips 使用教程

B站视频教程笔记基于VSCode和CMake实现C/C++开发 | Linux篇(gcc/g++)(安装配置使用详细教程)(VSCode教程)(CMake教程)(精!)

使用CMake检查列表是否包含特定条目的最佳方法

cmake基础教程(40)生成器表达式

cmake基础教程(40)生成器表达式

C++Cmake使用教程(看这一篇就够了)