初识CMake,如何编写一个CMake工程(上)

初识CMake,如何编写一个CMake工程(下)
https://blog.csdn.net/weixin_39956356/article/details/115260616?spm=1001.2014.3001.5501

笔者想分享CMake工程的原因?

  1. CMake的目标?
    跨平台自动编译,实现代码写一份,在Windows、Linux等系统,甚至不同处理器上,如X86、ARM等。从而做到“Write once, run everywhere”
  2. CMake的影响力?
    选择CMake作为整个项目的助手,无疑是一个正确的选择。比如熟知的openCV、openVINO、VTK、ITK、KDE等。
  3. 笔者接触CMake已经有一段时间,恰好看到一篇好的文章,借此以入门的姿态分享所得
  4. 最后会分享一些资料,代码在github可以下载,原文链接也会给出,欢迎批评指正。
  5. Github
    https://github.com/ve2102388688/myCmakeDemos

1 接触CMake

1.1 认识CMake被广泛的使用?

从下图可以看出,CMake现已经被广泛使用。

  1. 支持各种编译器GCC、Clang、MSVC等
  2. 支持大部分平台Windows、Linux、macOS等
  3. 支持底层语言C/C++等
    在这里插入图片描述

1.2 了解CMake运行流程

如下图,源文件该目录下的CMakeLists.txt在CMake作用下会生成Makefile文件,之后使用make命令进行编译、make test进行测试、make install进行安装。
CMake解决了什么问题?
显然,简化了人们编写Makefile文件的难度,对于大型应用写一个Makefile是不现实、极难维护的。
在这里插入图片描述

1.3 Make和Makefile是什么关系?

问题:当源文件比较多时,一般不适合直接通过gcc来编译代码,难道要一个一个写吗?如果修改了呢?文件移动了呢?
解决:需要一个自动化的编译工具。Make(GNU Make)是一个自动化软件,用于将源代码文件编译为可执行的二进制文件从而完成自动化编译。Make工具编译的时候需要Makefile文件提供编译规则,Makefile定义了一系列的编译规则,包括编译的先后顺序,哪些文件需要重新编译等操作。利用Make工具可以自动完成编译工作,如果修改了某几个源文件,则只重新编译这几个源文件。如果某个头文件被修改了,则重新编译所有包含该头文件的源文件。利用这种自动编译极大地提高了开发效率,避免了不必要的重新编译。

1.4 CMake与CMakeLists又是什么?

CMake是更加抽象的跨平台的项目管理工具,CMakeLists.txt文件在CMake作用下生成Makefile文件。
这里想简要谈下CMake语法

  1. CMakeLists.txt这个文件名不可拼错一个字符,否则编译失败
  2. 内置了很多预定义变量名PROJECT_BINARY_DIRCMAKE_C_FLAGSCMAKE_CXX_FLAGS
  3. 内置很多API,如aux_source_directoryinclude_directoriesprojecttarget_link_libraries等,注意这些API名字 大小写不敏感
  4. 这里一定要讲讲CMakeLists.txt工程结构,每一个目录下都应该有CMakeLists.txt!!!CMakeLists.txt会收集所有目录下的子CMakeLists.txt,这好像有点像Kconfig和.config意味哈

2 从源码到可执行程序的流程

为什么会分析这个问题?难道这对写CMakeLists.txt有影响?
答案是肯定的,因为CMakeLists.txt中的代码顺序是有要求的,正如编译器需要

预处理->编译->汇编->链接

上面的顺序是不可以颠倒,难道有先链接再编译???
在这里插入图片描述

3 一个源文件Demo1

源文件是一个计算指数的函数,这里主要分析CMakeLists.txt该怎么写?不关系源文件怎么写的哈,为了节省篇幅,下面就不会再贴源文件的代码了。所有的源文件代码都在Github,自己独立写CMakeLists.txt哈。

#include <stdio.h>
#include <stdlib.h>

/**
 * power - Calculate the power of number.
 * @param base: Base value.
 * @param exponent: Exponent value.
 *
 * @return base raised to the power exponent.
 */
double power(double base, int exponent)
{
    int result = base;
    int i;

    if (exponent == 0) {
        return 1;
    }
    
    for(i = 1; i < exponent; ++i){
        result = result * base;
    }

    return result;
}

int main(int argc, char *argv[])
{
    if (argc < 3){
        printf("Usage: %s base exponent \n", argv[0]);
        return 1;
    }
    double base = atof(argv[1]);
    int exponent = atoi(argv[2]);
    double result = power(base, exponent);
    printf("%g ^ %d is %g\n", base, exponent, result);
    return 0;
}

3.1 第一个CMakeLists.txt

首先你应该摒弃gcc 源文件 目标 -ixxx -Lxxx -lxxxx这种思维,因为CMake会自动为我们寻找编译器,你只需要关系

  1. 你的工程目录结构是什么?
  2. 你是否需要引用外部库?

比如,下面就是CMake为我们做的事,会自动检测可用编译器,根据你的配置文件自动寻找外部依赖库等。那我们的目的就是让CMake安装说明书翻译CMakeLists.txt即可。

-- The C compiler identification is GNU 7.5.0
-- The CXX compiler identification is GNU 7.5.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done

最核心的架子如下(必须要有)

  1. CMake需要一个最低版本的检查CMAKE_MINIMUM_REQUIRED,这里表示系统CMake版本至少是2.8,
    注意:
2. CMake中的API没有大小写之分,写成 cmake_minimum_required 也是可以的
3. VERSION必须大写,换言之预定义宏是不可以随意写的哈
  1. 之后需要建一个工程名,project(工程名)
  2. 构建目标文件即可,add_executable(可执行程序,源文件1,源文件2, ...)
# cmake语法关键字--大/小写无关
# cmake的最低版本号,注意VERSION需要大写
CMAKE_MINIMUM_REQUIRED(VERSION 2.8)

# 设置一个工程名字
project(Demo1)

# 目标可执行程序Demo, 需要编译main.cc
add_executable(Demo main.cc)

就三行代码,就可以执行cmake .&&make即可。很方便,只需要三句代码。
这里的cmake 路径(根CMakeLists.txt),注意这是根CMakeLists.txt路径!!!,下面是工程结构
值得一提的是,CMake不能自动追踪中间的编译文件,会生成很多中间文件,没有命令可以帮助我们做清理工作,好比make clean,这里提供一个del.sh脚本删除所有缓存,目的是为了让大家快速实践和修改CMakeLists.txt

.
├── CMakeLists.txt
├── del.sh
└── main.cc

4 同一个目录多个源文件Demo2

工程目录,这里有两个源文件哦,MathFunctions.ccmain.cc

.
├── CMakeLists.txt
├── MathFunctions.cc
├── MathFunctions.h
├── del.sh
└── main.cc

哈哈,很简单,在add_executable添加一个MathFunctions.cc即可,如下

# cmake语法关键字--大/小写无关
# cmake的最低版本号,注意VERSION需要大写
CMAKE_MINIMUM_REQUIRED(VERSION 2.8)

# 设置一个工程名字
project(Demo1)

# 将所有的源文件依次列出来
add_executable(Demo main.cc MathFunctions.cc)

4.1 aux_source_directory查找指定路径下的所有源文件

上面的做法很好,但是如果有100个源文件呢?一个一个添加吗?不
aux_source_directory(路径 宏名)表示搜索指定路径下的所有源文件,将其捆在一起,即宏名

# cmake语法关键字--大/小写无关
# cmake的最低版本号,注意VERSION需要大写
CMAKE_MINIMUM_REQUIRED(VERSION 2.8)

# 设置一个工程名字
project(Demo1)

aux_source_directory(. DIR_SRC)
add_executable(Demo ${DIR_SRC})

CMake 会将当前目录所有源文件的文件名赋值给变量 DIR_SRC,再指示变量 DIR_SRC中的源文件需要编译成一个名称为 Demo 的可执行文件。

5 多个目录Demo3

工程会很复杂,一般同main.cc同级下的源文件可以用上面的方法解决。但是有多个目录呢?
这里有一个math目录,怎么告诉CMake呢?我想至少有两点

  1. 在根CMakeLists.txt中告诉有一个子目录math
  2. 在math中再写一个CMakeLists.txt告诉根CMakeLists.txt需要处理我这个子目录
.
├── CMakeLists.txt
├── del.sh
├── main.cc
└── math
    ├── CMakeLists.txt
    ├── MathFunctions.cc
    └── MathFunctions.h

第一步:add_subdirectory添加子目录,当然这要放在add_executable,这是显然的,难道能调换过来吗?请看上面的编译流程,如果把顺序整反了,还在检查拼写错误是不应该。

# 编译其他目录下的文件,如math
add_subdirectory(math)

第二步: 建立子CMakeLists.txt,这时候你不应该在使用CMAKE_MINIMUM_REQUIREDproject,因为根CMakeLists.txt只有一个,子CMakeLists.txt只需要做好子目录应该做的事情,如:只需要编译math目录下的文件并生成静态库!!!
注意
这里只编译成动态、静态库哈,使用add_library(库名 源文件) 即可,不要使用add_executable哈,你要清楚你在干什么?链接一个exe?还是链接一个.lib,.dll还是.so,,a?

子CMakeLists.txt

# 将本目录下的所有文件全部编译成静态库
aux_source_directory(. MATH_DIR_SRC)

add_library(mathfun ${MATH_DIR_SRC})

根CMakeLists.txt

# cmake的最低版本号,注意VERSION需要大写
cmake_minimum_required(VERSION 2.8)

# 工程名
project(Demo3)

# 编译其他目录下的文件,如math
add_subdirectory(math)

# 编译当前目录下的文件
add_executable(Demo main.cc)

# 把其他目录下的静态、动态库链接进来
target_link_libraries(Demo mathfun)

这里多说一句,下面的代码编译器会做什么,我想大概是这样的

  1. 看见stdio.h、stdlib.h进行预处理,因为系统库最后只需要链接glibc中即可
  2. 看见math/MathFunctions.h,先去编译MathFunctions,将其编程成库,供main链接
  3. 重复第二步,其他文件…
  4. 因为main函数是最后才编译的,其实也就是把其他库的函数地址拼在一起。如果找不到就会报链接失败,如ld问题LNK2019
#include <stdio.h>
#include <stdlib.h>
#include "math/MathFunctions.h"
// 其他文件.....

输出的一部分
1 发现要编译target mathfun,因为add_subdirectory(math),链接成libmathfun.a供main使用
2. 编译main
3. Linking CXX executable Demo,自始至终可执行程序就一个,链接打包后就是Demo

Scanning dependencies of target mathfun
[ 25%] Building CXX object math/CMakeFiles/mathfun.dir/MathFunctions.cc.o
[ 50%] Linking CXX static library libmathfun.a
[ 50%] Built target mathfun
Scanning dependencies of target Demo
[ 75%] Building CXX object CMakeFiles/Demo.dir/main.cc.o
[100%] Linking CXX executable Demo
[100%] Built target Demo

6 根CMakeLists.txt流程

根据编译流程,根CMakeLists.txt书写流程大致如下,接下来会逐步完善

定义一个最低的版本号

定义一个工程
------------------------------------------
定义配置信息,如编译选项、Debug/Release、版本号、自定义宏名、条件编译
------------------------------------------

添加其他子目录

添加目标exe
------------------------------------------
添加install

添加测试test

7 CMake语法预设常用变量

这里提下CMAKE_ROOT,也就是安装路径,如/usr/share/cmake-3.10

变量含义
CMAKE_MAJOR_VERSIONcmake 主版本号;
CMAKE_MINOR_VERSIONcmake 次版本号;
CMAKE_C_FLAGS设置 C 编译选项;
CMAKE_CXX_FLAGS设置 C++ 编译选项;
PROJECT_SOURCE_DIR工程的根目录;
PROJECT_BINARY_DIR运行 cmake 命令的目录;
CMAKE_CURRENT_SOURCE_DIR当前CMakeLists.txt 所在路径;
CMAKE_CURRENT_BINARY_DIR目标文件编译目录;
EXECUTABLE_OUTPUT_PATH重新定义目标二进制可执行文件的存放位置
LIBRARY_OUTPUT_PATH重新定义目标链接库文件的存放位置
CMAKE_ROOT/usr/share/cmake-3.10

未按待续

8 参考文献

https://mp.weixin.qq.com/s/wLJkkLTup_Uf7X8MfDYNMQ

https://mp.weixin.qq.com/s/WglXaNNDETKKu6zICRYswQ

https://mp.weixin.qq.com/s/67lPVyWUXG0SPJm4AOHmBA

在线帮助文档
https://cmake.org/cmake/help/v3.10/

  • 35
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值