CMake 实例详解
CMake 是开源、跨平台的构建工具,可以让我们通过编写简单的配置文件去生成本地的 Makefile,这个配置文件是独立于运行平台和编译器的,这样就不用亲自去编写 Makefile 了,而且配置文件可以直接拿到其它平台上使用,无需修改,非常方便,从而做到 “Write once, run everywhere”。
CMakeLists.txt 是 CMake 的配置文件。notepad++ 支持 CMake 相关的语法,并提供自动提示,推荐使用它来编写 CMakeLists.txt。
- 编写 CMake 配置文件 CMakeLists.txt
- 执行命令
cmake PATH
生成 Makefile,PATH 是 CMakeLists.txt 所在的目录 - 使用
make
命令进行编译
Windows 版本的 CMake
CMake 下载
选择最新版本的 cmake-3.16.2-win64-x64.msi (msi 是需要安装的,zip 是免安装的)
https://cmake.org/download/
安装注意:在出现 “安装选项框中” 选中第三个,把 CMake 添加到当前用户的系统环境变量上
MinGW 下载
MinGW (Minimalist GNU for Windows),是一种 GCC 编译环境的安装程序
网盘下载,链接: https://pan.baidu.com/s/1dkO31D7klnBVJN8XOJ30sQ 提取码: m9dh
安装界面里勾选 mingw32-gcc-g+±bin 安装这一项即可
安装完成后,把 gcc.exe 所在的路径(如 C:\MinGW\bin)添加到 Windows 系统环境变量上
编译运行
写好 CMakeLists.txt 和 源文件
启动 Windows 命令终端,并进入到源码路径,执行以下命令
cmake -G"MinGW Makefiles" .
mingw32-make
.\demo_test.exe
Hello World
Hello CMake
Linux 版本的 CMake
本文主要讲述在 Linux 下如何使用 CMake 来编译我们的程序
安装CMake
sudo apt install cmake
查看版本
cmake -version
简单示例
单个源文件
准备好源文件,如 main.cpp
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Hello World\n");
printf("Hello CMake\n");
return 0;
}
编写 CMakeLists.txt,跟 main.cpp 同一目录
CMakeLists.txt 的语法比较简单,由命令、注释和空格组成,其中命令是不区分大小写的
符号 # 后面的内容被认为是注释
命令由命令名称、小括号和参数组成,参数之间使用空格进行间隔
cmake_minimum_required (VERSION 2.8)
project (demo1)
add_executable (demo main.cpp)
对于上面的 CMakeLists.txt 文件,依次出现了几个命令:
cmake_minimum_required
:指定运行此配置文件所需的 CMake 的最低版本project
:参数值是 demo1,该命令表示项目的名称是 demo1add_executable
:将名为 main.cpp 的源文件编译成一个名称为 demo 的可执行文件
编译项目
在当前目录执行cmake .
,成功生成了 Makefile,还有 cmake 运行时自动生成的文件
然后再使用make
命令编译得到 demo 可执行文件
同一目录,多个源文件
新建一个 add.cpp,添加一函数
int add(int a, int b)
{
return a + b;
}
main.cpp
#include <stdio.h>
#include "add.h"
int main(int argc, char *argv[])
{
int sum = 0;
sum = add(10, 20);
printf("sum = %d\n", sum);
return 0;
}
目录文件形式如下
02demo/
├── add.cpp
├── add.h
└── main.cpp
方法一
这时候,CMakeLists.txt 可以改成如下的形式,只需在 add_executable 里面添加 add.cpp 就可
cmake_minimum_required (VERSION 2.8)
project (demo2)
add_executable (demo main.cpp add.cpp)
这样写当然没什么问题,但是如果源文件很多,把所有源文件的名字都加进去将是一件烦人的工作
方法二
更省事的方法是使用aux_source_directory
命令,该命令会查找指定目录下的所有源文件,然后将结果存进指定变量名。其语法如下:
aux_source_directory(<dir> <variable>)
cmake_minimum_required (VERSION 2.8)
project (demo2)
aux_source_directory (./ DIR_SRCS)
add_executable (demo ${DIR_SRCS})
这样,CMake 会将当前目录所有源文件的文件名赋值给变量DIR_SRCS
,再指定变量DIR_SRCS
中的源文件编译成一个名称为 demo 的可执行文件
方法三
aux_source_directory
也存在弊端,它会把指定目录下的所有源文件都加进来,实际项目中可能有些是我们不需要的文件,此时我们可以使用set
命令去新建变量来存放需要的源文件,如下:
cmake_minimum_required (VERSION 2.8)
project (demo2)
set ( DIR_SRCS
./main.cpp
./add.cpp )
add_executable (demo ${DIR_SRCS})
多个目录,多个源文件
一般来说,当文件比较多时,我们会进行分类管理,根据功能把代码放在不同目录下,这样方便查找
现在进一步将 add.h 和 add.cpp 文件移动到 add 目录下,新建 sub.h 和 sub.cpp 放到 sub 目录下
目录文件形式如下
03demo/
├── add
│ ├── add.cpp
│ └── add.h
├── sub
│ ├── sub.cpp
│ └── sub.h
└── main.cpp
main.cpp
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main(int argc, char *argv[])
{
int sum = 0;
int diff = 0;
sum = add(10, 20);
printf("sum = %d\n", sum);
diff = sub(100, 30);
printf("diff = %d\n", diff);
return 0;
}
方法一
CMakeLists.txt 和 main.cpp 在同一目录下,内容修改成如下所示
cmake_minimum_required (VERSION 2.8)
project (demo3)
include_directories (./add ./sub)
aux_source_directory (./ DIR_SRCS)
aux_source_directory (./add DIR_SRCS1)
aux_source_directory (./sub DIR_SRCS2)
add_executable (demo ${DIR_SRCS} ${DIR_SRCS1} ${DIR_SRCS2})
该文件添加了下面的内容:
第 3 行,include_directories
是用来向工程添加多个指定头文件的搜索路径,路径之间用空格分隔
第 5/6 行,因为源文件分布在 2 个目录下,所以我们多使用了 2 次aux_source_directory
方法二(lib 库)
对于这种情况,也可以分别在 add 和 sub 目录里各编写一个 CMakeLists.txt 文件
为了方便,我们可以先将 add 和 sub 目录里的文件分别编译成 lib 库再由 main 函数调用
根目录中的 CMakeLists.txt
cmake_minimum_required (VERSION 2.8)
project (demo3)
include_directories (./add ./sub)
add_subdirectory (./add)
add_subdirectory (./sub)
aux_source_directory (./ DIR_SRCS)
add_executable (demo ${DIR_SRCS})
target_link_libraries (demo myadd mysub)
该文件添加了下面的内容:
第 4/5 行,add_subdirectory
指明本项目包含子目录 add 和 sub,这样当执行 cmake 时,就会进入子目录去找 CMakeLists.txt 来生成 Makefile
第 8 行,target_link_libraries
指明可执行文件 demo 需要链接一个名为 myadd 和 mysub 的链接库
这种写法默认是使用动态库,如果目录下只有静态库,这种写法就会去链接静态库
add 目录中的 CMakeLists.txt
aux_source_directory (./ DIR_LIB_SRCS)
add_library (myadd SHARED ${DIR_LIB_SRCS})
sub 目录中的 CMakeLists.txt
aux_source_directory (./ DIR_LIB_SRCS)
add_library (mysub STATIC ${DIR_LIB_SRCS})
在该文件中使用命令add_library
将 add 目录的源文件编译成动态库,将 sub 目录的源文件编译成静态库
add_library
第 1 个参数指定库的名字;第 2 个参数决定是动态还是静态,不写默认静态;第 3 个参数指定生成库的源文件。注意:SHARED 和 STATIC 是 cmake 的关键字,必须大写!
常用的组织结构
一般来说,我们习惯按如下方式来摆放文件,让我们把前面的文件重新组织下:
把源文件放到 src 目录下
把头文件放到 include 目录下
把生成的库文件放到 lib 目录下
把生成的对象文件放到 build 目录下
把最终输出的 elf 文件放到 bin 目录下
04demo/
├── bin
├── build
├── include
│ ├── add.h
│ └── sub.h
├── lib
└── src
├── lib_add
│ └── add.cpp
├── sub
│ └── sub.cpp
└── main.cpp
这里我们设定将 add.cpp 编译成静态库和动态库,sub.cpp 则与 main.cpp 一起编译
在最外层新建一个 CMakeLists.txt,最外层的 CMakeLists.txt 用于掌控全局,使用add_subdirectory
来添加要生成 elf 文件的源码目录即可,内容如下
cmake_minimum_required (VERSION 2.8)
project (demo4)
add_subdirectory (./src)
src 目录下,新建一个 CMakeLists.txt,内容如下
add_subdirectory (./lib_add)
aux_source_directory (./ DIR_SRCS1)
aux_source_directory (./sub DIR_SRCS2)
include_directories (../include)
link_directories (${PROJECT_SOURCE_DIR}/lib)
add_executable (demo ${DIR_SRCS1} ${DIR_SRCS2})
target_link_libraries (demo myadd)
set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
第 1 行,要先包含 lib_add 这个目录,因为需要依赖生成的库文件
第 5 行,link_directories
添加非标准库的搜索路径
第 8 行,EXECUTABLE_OUTPUT_PATH
和PROJECT_SOURCE_DIR
是 cmake 自带的预定义变量
EXECUTABLE_OUTPUT_PATH:目标二进制可执行文件的存放位置
PROJECT_SOURCE_DIR:当前工程的根目录
这里set
的意思是把存放 elf 文件的位置设置为工程根目录下的 bin 目录
lib_add 目录下,也要新建一个 CMakeLists.txt,内容如下
aux_source_directory (./ DIR_LIB_SRCS)
add_library (myadd_shared SHARED ${DIR_LIB_SRCS})
add_library (myadd_static STATIC ${DIR_LIB_SRCS})
set_target_properties (myadd_shared PROPERTIES OUTPUT_NAME "myadd")
set_target_properties (myadd_static PROPERTIES OUTPUT_NAME "myadd")
set (LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
第 4/5 行,set_target_properties
设置输出的名称
第 6 行,LIBRARY_OUTPUT_PATH
也是 cmake 自带的预定义变量
LIBRARY_OUTPUT_PATH:库文件的默认输出路径
这里set
的意思是把存放库文件的位置设置为工程目录下的 lib 目录
PS:上面使用set_target_properties
重新定义了库的输出名字,如果不用set_target_properties
也可以,那么库的名字就是add_library
里定义的名字,只是我们连续 2 次使用add_library
指定库名字时,这个名字不能相同,而set_target_properties
可以把名字设置为相同,只是最终生成的库文件后缀不同,这样相对来说会好一点
下面来运行 cmake,不过这次先让我们切到 build 目录下,运行cmake ..
此时 Makefile 会在 build 目录下生成,然后在 build 目录下运行make
这时我们再看 bin 目录,已经生成了 demo 可执行文件,lib 目录也生成了 libmyadd.a 和 libmyadd.so
这里解释一下为什么要在 build 目录下运行 cmake?从前面几个 case 中可以看到,如果不这样做,cmake 运行时生成的附带文件就会跟源码文件混在一起,这样会对程序的目录结构造成污染,而在 build 目录下运行 cmake,生成的附带文件就只会待在 build 目录下,如果我们不想要这些文件就可以直接清空 build 目录,非常方便
添加编译选项
有时编译程序时想添加一些编译选项,如 -Wall、-std=c++11 等,就可以使用add_compile_options
来操作,也可以通过set
命令修改CMAKE_CXX_FLAGS
或CMAKE_C_FLAGS
,这两个是 cmake 自带的预定义变量,用于设置编译选项
这两种方式的效果是一样的,但请注意它们还是有区别的:
add_compile_options
命令添加的编译选项是针对所有编译器的(包括 c 和 c++ 编译器),而set
命令设置CMAKE_C_FLAGS
或CMAKE_CXX_FLAGS
变量则是分别只针对 c 和 c++ 编译器的
整体目录结构如下
05demo/
├── bin
├── build
├── CMakeLists.txt
└── main.cpp
这里以一个简单程序来做演示,main.cpp 如下
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
auto sum = 100;
cout << "sum = " << sum << endl;
return 0;
}
CMakeLists.txt 内容如下
cmake_minimum_required (VERSION 2.8)
project (demo5)
#add_compile_options (-std=c++11 -Wall)
set (CMAKE_CXX_FLAGS "-std=c++11 -Wall ${CMAKE_CXX_FLAGS}")
aux_source_directory (./ DIR_SRCS)
add_executable (demo ${DIR_SRCS})
set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
cd 到 build 目录下,执行cmake .. && make
命令,就可以在 bin 目录下得到 elf 文件
添加控制选项
有时我们希望在编译代码时只编译一些指定的源码,这时可以使用 cmake 的option
命令
假设我们现在的工程会生成 2 个 bin 文件,main1 和 main2,现在整体结构如下
06demo/
├── bin
├── build
├── CMakeLists.txt
└── src
├── CMakeLists.txt
├── main1.cpp
└── main2.cpp
外层的 CMakeLists.txt 内容如下
cmake_minimum_required (VERSION 2.8)
project (demo6)
option (MYDEBUG "enable debug mode" OFF)
add_subdirectory (./src)
这里使用了option
命令,其第一个参数是这个 option 的名字,第二个参数是字符串,用来描述这个 option 是来干嘛的,第三个是 option 的值,ON
或OFF
,也可以不写,不写就是默认 OFF
src 目录下的 CMakeLists.txt,如下
add_executable (main1 main1.cpp)
if (MYDEBUG)
add_executable (main2 main2.cpp)
else()
message (STATUS "Currently is not in debug mode")
endif()
set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
这里使用了 if-else 根据 option 来决定是否编译 main2.cpp
message
为用户打印显示一条消息,可以用下述可选的关键字指定消息类型
(无) = 重要消息
STATUS
= 非重要消息
WARNING
= CMake 警告,会继续执行
AUTHOR_WARNING
= CMake 警告 (dev),会继续执行
SEND_ERROR
= CMake 错误,继续执行,但是会跳过生成的步骤
FATAL_ERROR
= CMake 错误,终止所有处理过程
其中 main1.cpp 和 main2.cpp 的内容如下
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("hello, this is main1\n");
return 0;
}
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("hello, this is main2\n");
return 0;
}
cd 到 build 目录下输入cmake .. && make
就可以只编译出 main1,如果想编译出 main2
- 直接修改 CMakeLists.txt,把 OFF 改成 ON,这种方法有点麻烦
- cd 到 build 目录,然后输入
cmake .. -DMYDEBUG=ON && make
,这样就可以编译出 main1 和 main2
支持 GDB
让 CMake 支持 gdb 的设置也很简单,只需要指定Debug
模式下开启-g
选项,加入 3 行代码就可以
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")
如下文件目录结构
07demo/
├── bin
├── build
├── CMakeLists.txt
└── src
├── CMakeLists.txt
└── main.cpp
外层 CMakeLists.txt 还是老样子
cmake_minimum_required (VERSION 2.8)
project (demo7)
add_subdirectory (./src)
src 目录下的 CMakeLists.txt 则加入 gdb 的设置,如下
aux_source_directory (./ DIR_SRCS)
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")
add_executable (demo ${DIR_SRCS})
set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
main 函数
#include <stdio.h>
int main(int argc, char *argv[])
{
int i = 10;
i += 20;
printf("hello, this is gdb test: i = %d\n", i);
return 0;
}
cd 到 build 目录下输入cmake .. && make
就可以编译出 demo,然后用命令gdb demo
来设置断点,验证一下
配置交叉编译器
CMake 在 ubuntu 系统下默认使用系统的 gcc、g++ 编译器,对于我们做嵌入式开发的,免不了需要配置交叉编译工具链,来看一下 CMake 怎么配置交叉工具链。下面以 arm-hisiv600-linux-gcc 为例说明一下
如下文件目录结构
08demo/
├── bin
├── build
├── CMakeLists.txt
└── src
├── CMakeLists.txt
└── main.cpp
外层 CMakeLists.txt 在一开始加入相关设置
cmake_minimum_required (VERSION 2.8)
# 添加配置
set (CMAKE_SYSTEM_NAME Linux)
set (CMAKE_C_COMPILER "arm-hisiv600-linux-gcc")
set (CMAKE_CXX_COMPILER "arm-hisiv600-linux-g++")
set (CMAKE_FIND_ROOT_PATH /opt/hisi-linux/x86-arm/arm-hisiv600-linux/target)
set (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set (CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set (CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
# 完成配置
project (demo8)
add_subdirectory (./src)
第 3 行,告知当前使用的是交叉编译方式,必须配置
第 4 行,指定 C 交叉编译器,必须配置
第 5 行,指定 C++ 交叉编译器,必须配置
第 6 行,指定交叉编译环境安装目录,非必须配置
第 7 行,从来不在指定目录下查找工具程序,非必须配置
第 8 行,只在指定目录下查找库文件,非必须配置
第 9 行,只在指定目录下查找头文件,非必须配置
src 目录下的 CMakeLists.txt 还是老样子,如下
aux_source_directory (./ DIR_SRCS)
add_executable (demo ${DIR_SRCS})
set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
main 函数
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("hello, cross-compiler test\n");
return 0;
}
cd 到 build 目录下输入cmake .. && make
就可以编译出 demo,然后拿到板子上跑一下,验证一下
执行 shell 命令
可以通过execute_process
调用一条或多条 shell 命令,这是在执行cmake
命令就会执行其中的 shell 命令
execute_process(COMMAND <shell命令> WORKING_DIRECTORY <这条shell命令执行的工作目录>)
cmake_minimum_required (VERSION 2.8)
project (demo9)
execute_process (
COMMAND mkdir xxx
COMMAND touch xxx/123.txt
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
如果要在执行make
阶段,去执行一些 shell 命令,可以用add_custom_target
和add_custom_command
这对于要在构建一个目标之前或之后执行一些操作非常有用,比如,我们的工程需要用到 log4cpp 的库,而我们拿到的只是一个 log4cpp-1.1.tar.gz 压缩包,所以需要用 shell 命令去解压并且编译它
cmake_minimum_required (VERSION 2.8)
project (demo9)
#增加一个没有输出的目标,并指定了ALL选项,使得它总是被构建
add_custom_target (
MyTarget ALL
COMMAND echo "Creating target MyTarget..."
DEPENDS hello.txt
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "This is add_custom_target"
)
#增加一个客制命令用来产生一个输出
add_custom_command (
OUTPUT hello.txt
COMMAND touch hello.txt
COMMAND echo "Generating hello.txt file..."
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "This is add_custom_command"
)
#为某个目标(如库或可执行程序)添加一个客制命令
add_custom_command (
TARGET MyTarget
PRE_BUILD
COMMAND echo "Executing a fake command..."
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "This command will be executed before building target MyTarget"
)
第 7 行:表示 MyTarget 依赖于 hello.txt
第 21 行:标记为在什么时候执行命令:编译前(PRE_BUILD
),编译后(POST_BUILD
),链接前(PRE_LINK
)
第 9/17/25 行:COMMENT
在构建的时候,该值会被当成信息在执行该命令之前显示
这类似于 Makefile 如下的定义
all:MyTarget
......
MyTarget:hello.txt
touch hello.txt
echo "Generating hello.txt file..."
执行make
时,打印如下
PS:上面的第 20~26 行这一段可以引申扩展一下,比如我编译完某个 lib 库之后,想把它拷贝到别的目录下,就可以使用POST_BUILD
来自定义命令,如
aux_source_directory (./ DIR_LIB_SRCS)
add_library (myadd SHARED ${DIR_LIB_SRCS})
add_custom_command (
TARGET myadd
POST_BUILD
COMMAND cp -rf libmyadd.so /tmp
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
总结
以上是对 CMake 的一点学习记录,通过简单的例子让大家入门 CMake,CMake的知识点还有很多,更多高级用法可以在网上搜索。总之,CMake 可以让我们不用去编写复杂的 Makefile,并且跨平台,是个非常强大并值得一学的工具
参考资料
https://blog.csdn.net/whahu1989/article/details/82078563
https://blog.csdn.net/zhuiyunzhugang/article/details/88142908
https://blog.csdn.net/weixin_36926794/article/details/80297929
https://blog.csdn.net/gubenpeiyuan/article/details/51096777
https://blog.csdn.net/bytxl/article/details/50634868