如何编写一个CMake工程
初识CMake,如何编写一个CMake工程(下)
https://blog.csdn.net/weixin_39956356/article/details/115260616?spm=1001.2014.3001.5501
笔者想分享CMake工程的原因?
- CMake的目标?
跨平台自动编译,实现代码写一份,在Windows、Linux等系统,甚至不同处理器上,如X86、ARM等。从而做到“Write once, run everywhere”
- CMake的影响力?
选择CMake作为整个项目的助手,无疑是一个正确的选择。比如熟知的openCV、openVINO、VTK、ITK、KDE等。 - 笔者接触CMake已经有一段时间,恰好看到一篇好的文章,借此以入门的姿态分享所得
- 最后会分享一些资料,代码在github可以下载,原文链接也会给出,欢迎批评指正。
- Github
https://github.com/ve2102388688/myCmakeDemos
1 接触CMake
1.1 认识CMake被广泛的使用?
从下图可以看出,CMake现已经被广泛使用。
- 支持各种编译器GCC、Clang、MSVC等
- 支持大部分平台Windows、Linux、macOS等
- 支持底层语言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语法
- CMakeLists.txt这个文件名
不可拼错一个字符,否则编译失败
- 内置了很多预定义变量名
PROJECT_BINARY_DIR
、CMAKE_C_FLAGS
、CMAKE_CXX_FLAGS
等 - 内置很多API,如
aux_source_directory
、include_directories
、project
、target_link_libraries
等,注意这些API名字 大小写不敏感 - 这里一定要讲讲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会自动为我们寻找编译器,你只需要关系
- 你的工程目录结构是什么?
- 你是否需要引用外部库?
比如,下面就是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
最核心的架子如下(必须要有):
- CMake需要一个最低版本的检查
CMAKE_MINIMUM_REQUIRED
,这里表示系统CMake版本至少是2.8,
注意:
2. CMake中的API没有大小写之分,写成 cmake_minimum_required 也是可以的
3. VERSION必须大写,换言之预定义宏是不可以随意写的哈
- 之后需要建一个工程名,
project(工程名)
- 构建目标文件即可,
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.cc
和main.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呢?我想至少有两点
- 在根CMakeLists.txt中告诉有一个子目录math
- 在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_REQUIRED
、project
,因为根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)
这里多说一句,下面的代码编译器会做什么,我想大概是这样的
- 看见stdio.h、stdlib.h进行预处理,因为系统库最后只需要链接glibc中即可
- 看见math/MathFunctions.h,先去编译MathFunctions,将其编程成库,供main链接
- 重复第二步,其他文件…
- 因为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_VERSION | cmake 主版本号; |
CMAKE_MINOR_VERSION | cmake 次版本号; |
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/