CMake
是一个跨平台的项目构建工具。我们所熟知的项目构建工具还有Makefile
(通过 make
命令进行项目的构建),大多数 IDE
软件都集成了 make
,比如:VS
的 nmake
、Linux
下的 GNU make
、Qt
的 qmake
等,这些 Make
工具遵循着不同的规范和标准,所执行的 Makefile
格式也千差万别。这样就带来了一个严峻的问题:如果软件想跨平台,必须要保证能够在不同平台编译。而如果使用上面的 Make
工具,就得为每一种标准写一次 Makefile
,这将是一件让人抓狂的工作。如果自己动手写 makefile
,会发现,makefile
通常依赖于当前的编译平台,而且编写 makefile
的工作量比较大,解决依赖关系时也容易出错。
而 CMake
就是针对上面问题所设计的工具, 其允许开发者发者编写一种平台无关的 CMakeList.txt
文件来定制整个工程的编译流程,再根据编译平台,自动生成本地化的 Makefile
和工程文件,从而做到“Write once, run everywhere”。最后用户只需 make
编译即可,所以可以把 CMake
看成一款自动生成 Makefile
的工具,其编译流程如下图:
Makefile
构建项目的过程cmake
构建项目的过程注意:
cmake PATH
命令中的PATH
是CMakeLists.txt
所在的目录。
介绍完 CMake
的是什么以及它的作用之后,再来总结一下它的几点优势:
cmake
编写特定功能的模块,扩充 cmake
功能cmake
目前已经成为各大 Linux
发行版提供的组件,比如 Everest
直接在系统中包含,Fedora
在 extra
仓库中提供,所以,需要自己动手安装的可能性很小。如果你使用的操作系统(比如 Windows
或者某些 Linux
版本)没有提供 cmake
或者包含的版本较旧,建议你直接从 cmake
官方网站下载安装。
下载地址:https://cmake.org/download
在这个页面,提供了源代码的下载以及针对各种不同操作系统的二进制下载,可以选择适合自己操作系统的版本下载安装。因为各个系统的安装方式和包管理格式有所不同,在此就不再赘述了,相信一定能够顺利安装 cmake
。
CMake
支持大写、小写、混合大小写的指令,即指令是大小写无关的,参数和变量是大小写相关的。
CMake
使用 #
进行行注释,可以放在任何位置。
# 这是一个 CMakeLists.txt 文件
CMAKE_MINIMUM_REQUIRED(VERSION 3.0.0)
CMake
使用 #[[ ]]
形式进行块注释。
#[[ 这是一个 CMakeLists.txt 文件。
这是一个 CMakeLists.txt 文件
这是一个 CMakeLists.txt 文件]]
CMAKE_MINIMUM_REQUIRED(VERSION 3.0.0)
首先,在 /backup
目录建立一个 cmake
目录,用来放置我们所有演示示例。
mkdir -p /backup/cmake
然后在 cmake
建立第一个练习目录 t1
cd /backup/cmake
mkdir t1
cd t1
在 t1
目录建立 main.c
和 CMakeLists.txt
(注意文件名大小写):
main.c
文件内容:
//main.c
#include
int main()
{
printf(“Hello World from t1 Main!\n”);
return 0;
}
CmakeLists.txt
文件内容:
PROJECT (HELLO)
SET(SRC_LIST main.c)
MESSAGE(STATUS "This is BINARY dir " ${HELLO_BINARY_DIR})
MESSAGE(STATUS "This is SOURCE dir "${HELLO_SOURCE_DIR})
ADD_EXECUTABLE(hello ${SRC_LIST})
命令解析:
PROJECT(projectname [CXX] [C] [Java])
你可以用这个指令定义工程名称,并可指定工程支持的语言,默认情况表示支持所有语言。如果不需要这些都是可以忽略的,只需要指定出工程名字即可。这个指令隐式的定义了两个 cmake
变量:
这里就是 HELLO_BINARY_DIR
和 HELLO_SOURCE_DIR
(所以 CMakeLists.txt
中两个 MESSAGE
指令可以直接使用了这两个变量),因为采用的是内部编译,两个变量目前指的都是工程所在路径/backup/cmake/t1
,后面我们会讲到外部编译,两者所指代的内容会有所不同。
同时 cmake
系统也帮助我们预定义了两个 cmake
变量:
PROJECT_BINARY_DIR
,值等于
PROJECT_SOURCE_DIR
,值等于
为了统一起见,建议以后直接使用 PROJECT_BINARY_DIR
,PROJECT_SOURCE_DIR
,即使修改了工程名称,也不会影响这两个变量。如果使用了
,修改工程名称后,需要同时修改这些变量。
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]]) #[]中的参数为可选项, 如不需要可以不写
VAR
:变量名
VALUE
:变量值
现阶段,你只需要了解 SET
指令可以用来显式的定义变量即可。
比如我们用到的是 SET(SRC_LIST main.c)
,如果有多个源文件,也可以定义成: SET(SRC_LIST main.c t1.c t2.c)
。
注意:SET指令的参数使用括弧括起,参数之间使用空格或分号分开,如:
SET(SRC_LIST main.c t1.c t2.c)
或SET(SRC_LIST main.c t1.c;t2.c)
拓展:
cmake
的语法还是比较灵活而且考虑到各种情况,比如SET(SRC_LIST main.c)
也可以写成SET(SRC_LIST “main.c”)
是没有区别的,但是假设一个源文件的文件名是fu nc.c
(文件名中间包含了空格)。这时候就必须使用双引号,如果写成了SET(SRC_LIST fu nc.c)
,就会出现错误,提示你找不到fu
文件和nc.c
文件。这种情况,就必须写成:SET(SRC_LIST “fu nc.c”)
MESSAGE([SEND_ERROR | STATUS | FATAL_ERROR] "message to display"
...)
这个指令用于向终端输出用户定义的信息,包含了三种类型:
SEND_ERROR,产生错误,生成过程被跳过
SATUS,输出前缀为--
的信息
FATAL_ERROR,立即终止所有 cmake
过程
我们在这里使用的是 STATUS
信息输出,演示了由 PROJECT
指令定义的两个隐式变量HELLO_BINARY_DIR
和 HELLO_SOURCE_DIR
。
ADD_EXECUTABLE(executable_file source_file)
project
中的项目名没有任何关系;
分隔定义了这个工程会生成一个文件名为 hello
的可执行文件,相关的源文件是 SRC_LIST
中定义的源文件列表, 本例中你也可以直接写成 ADD_EXECUTABLE(hello main.c)
。
拓展:在本例我们使用了
${}
来引用变量,这是cmake
的变量应用方式,但是,有一些例外,比如在IF
控制语句,变量是直接使用变量名引用,而不需要${}
。如果使用了${}
去应用变量,其实IF
会去判断名为${}
所代表的变量,那当然是不存在的了。
所有的文件创建完成后,t1
目录中应该存在 main.c
和 CMakeLists.txt
两个文件,接下来我们来构建这个工程,在这个目录运行:
[root@localhost t1]# cmake .
CMake Warning (dev) at CMakeLists.txt:1 (PROJECT):
cmake_minimum_required() should be called prior to this top-level project()
call. Please see the cmake-commands(7) manual for usage documentation of
both commands.
This warning is for project developers. Use -Wno-dev to suppress it.
-- This is BINARY dir /backup/cmake/t1
-- This is SOURCE dir /backup/cmake/t1
-- Configuring done (0.0s)
-- Generating done (0.0s)
-- Build files have been written to: /backup/cmake/t1
注意:
CMAKE_MINIMUM_REQUIRED
命令是可选的,我们可以不写(但是会有警告信息),在有些情况下,如果CMakeLists.txt
文件中使用了一些高版本cmake
特有的一些命令的时候,就需要加上这样一行,提醒用户升级到该版本之后再执行 cmake。
再让我们看一下目录中的内容,你会发现,系统自动生成了:
CMakeFiles, CMakeCache.txt, cmake_install.cmake
等文件,并且生成了 Makefile
。现在不需要理会这些文件的作用,以后你也可以不去理会。最关键的是,它自动生成了 Makefile
。然后进行工程的实际构建,在这个目录输入 make
命令,大概会得到如下的输出:
[root@localhost t1]# make
[ 50%] Building C object CMakeFiles/hello.dir/main.c.o
[100%] Linking C executable hello
[100%] Built target hello
如果你需要看到 make
构建的详细过程,可以使用 make VERBOSE=1
或者 VERBOSE=1 make
命令来进行构建。
这时候,我们需要的目标文件 hello
已经构建完成,位于当前目录,尝试运行一下:
[root@localhost t1]# ls
CMakeCache.txt CMakeFiles cmake_install.cmake CMakeLists.txt hello main.c Makefile
[root@localhost t1]# ./hello
Hello World from t1 Main!
恭喜您,到这里为止您已经完全掌握了 cmake
基本的使用方法。
3.2.1.1
的例子展示的是“内部构建”,相信看到生成的临时文件比您的代码文件还要多的时候,估计这辈子你都不希望再使用内部构建。cmake
强烈推荐的是外部构建(out-of-source build)
。
对于 cmake
,内部编译上面已经演示过了,它生成了一些无法自动删除的中间文件(使用 make clean
也无法清除),所以,引出了我们对外部编译的探讨,外部编译的过程如下:
首先,请清除 t1
目录中除 main.c
与 CmakeLists.txt
之外的所有中间文件,最关键的是清除掉 CMakeCache.txt
。
在 t1
目录中建立 build
目录,当然你也可以在任何地方建立 build
目录,不一定必须在工程目录中。
进入 build
目录,运行 cmake ..
(注意..
代表父目录,因为父目录存在我们需要的CMakeLists.txt
,如果你在其他地方建立了 build
目录,需要运行 cmake
<工程的全 路径>),查看一下 build
目录,就会发现了生成了编译需要的 Makefile
以及其他的中间文件。
运行 make
构建工程,就会在当前目录(build
目录)中获得目标文件 hello
。这样通过cmake
和make
生成的所有文件就全部和项目源文件隔离开了,各回各家,各找各妈。
上述过程就是所谓的 out-of-source
外部编译,一个最大的好处是,对于原有的工程没有任何影响,所有动作全部发生在编译目录。通过这一点,也足以说服我们全部采用外部编译方式构建工程。
注意:通过外部编译进行工程构建,
HELLO_SOURCE_DIR
仍然指代工程路径,即/backup/cmake/t1
,而HELLO_BINARY_DIR
则指代编译路径,即/backup/cmake/t1/build
[root@localhost t1]# make clean
[root@localhost t1]# ls
CMakeCache.txt CMakeFiles cmake_install.cmake CMakeLists.txt main.c Makefile
[root@localhost t1]# ls
CMakeCache.txt CMakeFiles cmake_install.cmake CMakeLists.txt main.c Makefile
[root@localhost t1]# rm -f CMakeCache.txt cmake_install.cmake Makefile
[root@localhost t1]# ls
CMakeFiles CMakeLists.txt main.c
[root@localhost t1]# rm -rf CMakeFiles
[root@localhost t1]# ls
CMakeLists.txt main.c
[root@localhost t1]# mkdir build
[root@localhost t1]# ls
build CMakeLists.txt main.c
[root@localhost t1]# cd build/
[root@localhost build]# cmake ..
CMake Warning (dev) at CMakeLists.txt:1 (PROJECT):
cmake_minimum_required() should be called prior to this top-level project()
call. Please see the cmake-commands(7) manual for usage documentation of
both commands.
This warning is for project developers. Use -Wno-dev to suppress it.
-- The C compiler identification is GNU 8.5.0
-- The CXX compiler identification is GNU 8.5.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- This is BINARY dir /backup/cmake/t1/build
-- This is SOURCE dir /backup/cmake/t1
-- Configuring done (0.8s)
-- Generating done (0.0s)
-- Build files have been written to: /backup/cmake/t1/build
[root@localhost build]# ls
CMakeCache.txt CMakeFiles cmake_install.cmake Makefile
[root@localhost build]# ls ../
build CMakeLists.txt main.c
[root@localhost build]# make
[ 50%] Building C object CMakeFiles/t1.dir/main.c.o
[100%] Linking C executable t1
[100%] Built target t1
[root@localhost build]# ls
CMakeCache.txt CMakeFiles cmake_install.cmake Makefile t1
[root@localhost build]# ./t1
Hello World from t1 Main!
在大型项目中,代码组织和管理是非常重要的,特别是跨平台项目。CMake
作为一个功能强大的构建工具,支持模块化项目管理,允许我们将项目分割成多个子目录,每个子目录都有自己的 CMakeLists.txt
文件,从而实现更好的代码结构和可维护性。
让我们通过具体的示例演示 CMake
管理多目录工程模块化构建:
# 工程文件目录结构
[root@localhost multi_dir]# tree -L 2
.
├── app
│ └── main.c
├── build
├── CMakeLists.txt
├── hello
│ ├── CMakeLists.txt
│ ├── include
│ └── src
└── world
├── CMakeLists.txt
├── include
└── src
8 directories, 4 files
文件源码如下:
hello
子目录头文件:hello/include/hello.h
。
#ifndef HELLOWORLD_HELLO_H
#define HELLOWORLD_HELLO_H
extern void hello(void);
#endif //HELLOWORLD_HELLO_H
hello
子目录源文件:hello/src/hello.c
。
#include "hello.h"
#include
void hello()
{
printf("hello.\n");
}
hello
子目录CMakeLists.txt
文件:hello/CMakeLists.txt
。
# 添加头文件路径
include_directories(./include)
# 设置变量DIR_SRCS,其值为hello/src/下的源文件hello.c
set(DIR_SRCS ./src/hello.c)
# 生成动态链接库
add_library(hello SHARED ${DIR_SRCS})
在该文件中使用命令 add_library
将 /hello/src
目录中的源文件编译为动态链接库。
world
子目录头文件:world/include/world.h
。
#ifndef HELLOWORLD_WORLD_H
#define HELLOWORLD_WORLD_H
extern void world(void);
#endif //HELLOWORLD_WORLD_H
world
子目录源文件:world/src/world.c
。
#include "world.h"
#include
void world()
{
printf("world.\n");
}
world
子目录CMakeLists.txt
文件:world/CMakeLists.txt
。
# 添加头文件路径
include_directories(./include)
# 设置变量DIR_SRCS,其值为world/src/下的源文件world.c
set(DIR_SRCS ./src/world.c)
# 生成静态链接库
add_library(world STATIC ${DIR_SRCS})
app
子目录主源文件:app/main.c
。
#include "hello.h"
#include "world.h"
int main()
{
hello();
world();
return 0;
}
顶级目录下的主 CMakeLists.txt
文件:CMakeLists.txt
。
# CMake最低版本号要求
cmake_minimum_required(VERSION 3.0)
# 项目信息,随便写
project(HelloWorld)
# #设置C/C++版本(如c99,c++11,c++17等版本),下面表示使用c99版本
set(CMAKE_C_STANDARD 99)
# 指定目录添加到编译器的头文件搜索路径之下,指定的目录被解释成当前源码路径的相对路径。
# 当然也可以使用绝对路径和自定义的变量。默认情况下,include_directories命令会将目录
# 添加到列表最后(AFTER选项)。不过,可以通过命令设置CMAKE_INCLUDE_DIRECTORIES_BEFORE
# 变量为ON来改变它的默认行为,将目录添加到列表前面。也可以在每次调用include_directories命令时
# 使用AFTER或BEFORE选项来指定是添加到列表的前面或者后面。
include_directories(hello/include world/include)
# 设置变量DIR_SRCS,其值为app/下的源文件main.c
set(DIR_SRCS ./app/main.c)
# 添加子目录hello和world,这样hello和world各自目录下的CMakeLists.txt文件和源代码也会被处理
add_subdirectory(hello)
add_subdirectory(world)
# 指定静态库路径,${PROJECT_SOURCE_DIR}表示主CMakeLists.txt所在的文件夹路径,
# 即项目所在根目录文件路径
link_directories(${PROJECT_SOURCE_DIR}/world)
# 链接子目录生成的静态库libworld.a,指定的时候一般会掐头(lib)去尾(.a)
link_libraries(world)
# 指定生成目标
add_executable(HelloWorld ${DIR_SRCS})
# 链接子目录生成的动态库 libhello.so,指定的时候一般会掐头(lib)去尾(.so)
target_link_libraries(HelloWorld hello)
在 build
目录下使用 cmake ..
编译工程,执行生成的可执行程序 HelloWorld
,如下:
[root@localhost build]# cmake ..
-- The C compiler identification is GNU 8.5.0
-- The CXX compiler identification is GNU 8.5.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.4s)
-- Generating done (0.0s)
-- Build files have been written to: /backup/cmake/multi_dir/build
[root@localhost build]# make
[ 16%] Building C object world/CMakeFiles/world.dir/src/world.c.o
[ 33%] Linking C static library libworld.a
[ 33%] Built target world
[ 50%] Building C object hello/CMakeFiles/hello.dir/src/hello.c.o
[ 66%] Linking C shared library libhello.so
[ 66%] Built target hello
[ 83%] Building C object CMakeFiles/HelloWorld.dir/app/main.c.o
[100%] Linking C executable HelloWorld
[100%] Built target HelloWorld
[root@localhost build]# tree -L 2
.
├── CMakeCache.txt
├── CMakeFiles
│ ├── 3.26.5
│ ├── cmake.check_cache
│ ├── CMakeConfigureLog.yaml
│ ├── CMakeDirectoryInformation.cmake
│ ├── CMakeScratch
│ ├── HelloWorld.dir
│ ├── Makefile2
│ ├── Makefile.cmake
│ ├── pkgRedirects
│ ├── progress.marks
│ └── TargetDirectories.txt
├── cmake_install.cmake
├── hello
│ ├── CMakeFiles
│ ├── cmake_install.cmake
│ ├── libhello.so
│ └── Makefile
├── HelloWorld
├── Makefile
└── world
├── CMakeFiles
├── cmake_install.cmake
├── libworld.a
└── Makefile
9 directories, 17 files
[root@localhost build]# ./HelloWorld
hello.
world.
有些时候我们编写的源代码并不需要将他们编译生成可执行程序,而是生成一些静态库或动态库提供给第三方使用,上述示例即是如此,同时生成了静态库和动态库供主程序调用,生成最终的可执行程序。
接下来,让我们介绍一下制作静态库和动态库的命令,如下:
在 cmake
中,如果要制作静态库,需要使用的命令如下:
add_library(库名称 STATIC 源文件1 [源文件2] ...)
在 Linux
中,静态库名字分为三部分:lib
+库名字
+.a
,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。
注意:在
Windows
中虽然库名和Linux
格式不同,但也只需指定出名字即可。
在 cmake
中,如果要制作动态库,需要使用的命令如下:
add_library(库名称 SHARED 源文件1 [源文件2] ...)
在 Linux
中,动态库名字分为三部分:lib
+库名字
+.so
,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。
注意:在
Windows
中虽然库名和Linux
格式不同,但也只需指定出名字即可。
制作完静态库或者动态库以后,则需要链接才能使用,链接命令如下:
在 cmake
中,链接静态库的命令如下:
link_libraries( [...])
注意:静态库名字,即掐头(
lib
)去尾(.a
)之后的名字xxx
。
如果该静态库不是系统提供的(自己制作或者使用第三方提供的静态库)可能出现静态库找不到的情况,此时可以将静态库的路径也指定出来:
link_directories()
注意:
link_directories
在CMake
中可以用于指定静态库位置;也可以在生成可执行程序之前,通过该命令指定出要链接的动态库的位置。
在 cmake
中链接动态库的命令如下:
target_link_libraries(
- ...
[ - ...]...)
target:指定要加载动态库的文件的名字
PRIVATE|PUBLIC|INTERFACE:动态库的访问权限,默认为PUBLIC
PUBLIC
:在public后面的库会被Link到前面的target中,并且里面的符号也会被导出,提供给第三方使用。PRIVATE
:在private后面的库仅被link到前面的target中,并且终结掉,第三方不能感知你调了啥库INTERFACE
:在interface后面引入的库不会被链接到前面的target中,只会导出符号。如果各个动态库之间没有依赖关系,无需做任何设置,三者没有没有区别,一般无需指定,使用默认的 PUBLIC 即可。
动态库的链接具有传递性
,如果动态库 A 链接了动态库B、C,动态库D链接了动态库A,此时动态库D相当于也链接了动态库B、C,并可以使用动态库B、C中定义的方法。
target_link_libraries(A B C)
target_link_libraries(D A)
【拓展:动态库的链接和静态库的链接的区别】:
静态库会在生成可执行程序的链接阶段被打包到可执行程序中,所以可执行程序启动,静态库就被加载到内存中了。 动态库在生成可执行程序的链接阶段不会被打包到可执行程序中,当可执行程序被启动并且调用了动态库中的函数的时候,动态库才会被加载到内存 因此,在
cmake
中指定要链接动态库的时候,应该将命令写到生成了可执行文件之后:...
# 指定生成目标
add_executable(HelloWorld ${DIR_SRCS})
# 链接子目录生成的动态库 libhello.so,指定的时候一般会掐头(lib)去尾(.so)
target_link_libraries(HelloWorld hello)在
target_link_libraries(HelloWorld hello)
中:
HelloWorld:
对应的是最终生成的可执行程序的名字hello
:这是可执行程序要加载的动态库,这个库是自己制作的动态库,全名为libhello.so
,在指定的时候一般会掐头(lib
)去尾(.so
)。
温馨提示:使用
target_link_libraries
命令可以链接动态库,也可以链接静态库文件。
至此,我们通过两个示例,分别演示了单个目录和多个目录的工程项目文件的 cmake
实战编译,看到这里,如果对上面内容全部理解,则算是对 cmake
入门了,后续文章会进一步精进 cmake
的用法。