一、cmake简介
Makefile
带来的好处就是——“自动化编译”,一旦写好,只需要一个
make
命令,整个工程完全按照 Makefile 文件定义的编译规则进行自动编译,极大的提高了软件开发的效率。大都数的
IDE
都有这个工具, 譬如 Visual C++
的
nmake
、
linux
下的
GNU make
、
Qt
的
qmake
等等,这些
make
工具遵循着不同的规范和标准,对应的 Makefile
文件其语法、格式也不相同,这样就带来了一个严峻的问题:如果软件想跨平台,必须要保证能够在不同平台下编译,而如果使用上面的 make
工具,就得为每一种标准写一次
Makefile
,这将是一件让人抓狂的工作。
而
cmake
就是针对这个问题所诞生,允许开发者编写一种与平台无关的
CMakeLists.txt 文件来制定整个工程的编译流程,再根据具体的编译平台,生成本地化的 Makefile 和工程文件,最后执行 make
编译。因此,对于大多数项目,我们应当考虑使用更自动化一些的 cmake
或者
autotools
来生成
Makefile
,而不是直接动手编写 Makefile
。
1、cmake是什么
cmake
是一个跨平台的自动构建工具,
cmake
的诞生主要是为了解 决直接使用 make+Makefile
这种方式无法实现跨平台的问题,所以
cmake
是可以实现跨平台的编译工具,这是它最大的特点,当然除了这个之外,cmake
还包含以下优点:
⚫
开放源代码。我们可以直接从
cmake
官网
https://cmake.org/
下载到它的源代码;
⚫
跨平台。
cmake
并不直接编译、构建出最终的可执行文件或库文件,它允许开发者编写一种与平台无关的 CMakeLists.txt
文件来制定整个工程的编译流程,
cmake
工具会解析
CMakeLists.txt
文件语法规则,再根据当前的编译平台,生成本地化的 Makefile
和工程文件,最后通过
make
工具来编译整个工程;所以由此可知,cmake
仅仅只是根据不同平台生成对应的
Makefile
,最终还是通过
make工具来编译工程源码,但是 cmake
却是跨平台的。
⚫
语法规则简单。
Makefile
语法规则比较复杂,对于一个初学者来说,通常并不那么友好,并且
Makefile
语法规则在不同平台下往往是不一样的;而
cmake
依赖的是
CMakeLists.txt
文件,该文件 的语法规则与平台无关,并且语法规则简单、容易理解!cmake
工具通过解析
CMakeLists.txt
自动帮我们生成 Makefile
,这样就不需要我们自己手动编写
Makefile
了。
2、cmake和Makefile
直观上理解,
cmake
就是用来产生
Makefile
的工具,解析
CMakeLists.txt
自动生成
Makefile
:
3、使用方法
cmake
就是一个工具命令,在
Ubuntu
系统下通过
apt-get
命令可以在线安装,如下所示:
sudo apt-get install cmake
安装完
cmake
工具之后,接着我们就来学习如何去使用
cmake
。
cmake
官方也给大家提供相应教程,链接地址如下所示:
https://cmake.org/documentation/
//文档总链接地址
https://cmake.org/cmake/help/latest/guide/tutorial/index.html
//培训教程
示例一:单个源文件
单个源文件的程序通常是最简单的,一个经典的
C
程序“
Hello World
”,如何用
cmake
来进行构建呢?
现在我们需要新建一个
CMakeLists.txt
文件,
CMakeLists.txt
文件会被
cmake
工具解析,就好比
Makefile 文件会被 make
工具解析一样;
CMakeLists.txt
创建完成之后,在文件中写入如下内容:
project(HELLO)
add_executable(hello ./main.c)
写入完成之后,保存退出,当前工程目录结构如下所示:
├── CMakeLists.txt
└── main.c
在我们的工程目录下有两个文件,源文件
main.c
和
CMakeLists.txt
,接着我们在工程目录下直接执行 cmake 命令,如下所示:
cmake ./
cmake
后面携带的路径指定了
CMakeLists.txt
文件的所在路径,执行结果如下所示:
执行完
cmake
之后,除了源文件
main.c
和
CMakeLists.txt
之外,可以看到当前目录下生成了很多其它 的文件或文件夹,包括:CMakeCache.txt
、
CmakeFiles
、
cmake_install.cmake
、
Makefile
,重点是生成了这个 Makefile 文件,有了
Makefile
之后,接着我们使用
make
工具编译我们的工程:
通过
make
编译之后得到了一个可执行文件
hello
,这个名字是在
CMakeLists.txt
文件中指定的。
CMakeLists.txt
文件
上面我们通过了一个非常简单例子向大家演示了如何使用
cmake
,重点在于去编写一个
CMakeLists.txt文件,现在来看看 CMakeLists.txt
文件中写的都是什么意思。
⚫
第一行
project(HELLO)
project
是一个命令,命令的使用方式有点类似于
C
语言中的函数,因为命令后面需要提供一对括号, 并且通常需要我们提供参数,多个参数使用空格分隔而不是逗号“,
”。
project
命令用于设置工程的名称,括号中的参数
HELLO
便是我们要设置的工程名称;设置工程名称并不是强制性的,但是最好加上。
⚫
第二行
add_executable(hello ./main.c)
add_executable
同样也是一个命令,用于生成一个可执行文件,在本例中传入了两个参数,第一个参数表示生成的可执行文件对应的文件名,第二个参数表示对应的源文件;所以 add_executable(hello ./main.c)
表示需要生成一个名为 hello
的可执行文件,所需源文件为当前目录下的
main.c
。
使用
out-of-source
方式构建
在上面的例子中,
cmake
生成的文件以及最终的可执行文件
hello
与工程的源码文件
main.c
混在了一起,这使得工程看起来非常乱,当我们需要清理 cmake
产生的文件时将变得非常麻烦,这不是我们想看到的;我们需要将构建过程生成的文件与源文件分离开来,不让它们混杂在一起,也就是使用 out-of-source
方式构建。
将
cmake
编译生成的文件清理下,然后在工程目录下创建一个
build
目录,如下所示:
├── build
├── CMakeLists.txt
└── main.c
然后进入到
build
目录下执行
cmake
:
cd build/
cmake ../
make
这样
cmake
生成的中间文件以及
make
编译生成的可执行文件就全部在
build
目录下了,如果要清理工程,直接删除 build
目录即可,这样就方便多了。
示例二:多个源文件
一个源文件的例子似乎没什么意思,我们再加入一个
hello.h
头文件和
hello.c
源文件。在
hello.c
文件中定义了一个函数 hello
,然后在
main.c 源文件中将会调用该函数,然后准备好
CMakeLists.txt
文件。
project(HELLO)
set(SRC_LIST main.c hello.c)
add_executable(hello ${SRC_LIST})
工程目录结构如下所示:
├──
build //文件夹
├──
CMakeLists.txt
├──
hello.c
├──
hello.h
└──
main.c
同样,进入到
build
目录下,执行
cmake
、再执行
make
编译工程,最终就会得到可执行文件
hello
。
在本例子中,
CMakeLists.txt
文件中使用到了
set
命令,
set
命令用于设置变量,如果变量不存在则创建该变量并设置它;在本例中,我们定义了一个 SRC_LIST
变量,
SRC_LIST
变量是一个源文件列表,记录生成可执行文件 hello
所需的源文件
main.c
和
hello.c
,而在
add_executable
命令引用了该变量;当然我们也可以不去定义 SRC_LIST
变量,直接将源文件列表写在
add_executable
命令中,如下:
add_executable(hello main.c hello.c)
示例三:生成库文件
在本例中,除了生成可执行文件
hello
之外,我们还需要将
hello.c
编译为静态库文件或者动态库文件, 在示例二的基础上对 CMakeLists.txt
文件进行修改,如下所示:
project(HELLO)
add_library(libhello hello.c)
add_executable(hello main.c)
target_link_libraries(hello libhello)
进入到
build
目录下,执行
cmake
、再执行
make
编译工程,编译完成之后,在
build
目录下就会生成可执行文件 hello
和库文件,如下所示:
├──
build
│ ├──
hello
│ └──
liblibhello.a
├──
CMakeLists.txt
├──
hello.c
├──
hello.h
└──
main.c
本例中我们使用到了
add_library
命令和
target_link_libraries
命令。
add_library 命令用于生成库文件,在本例中我们传入了两个参数,第一个参数表示库文件的名字,需要注意的是,这个名字是不包含前缀和后缀的名字;在 Linux
系统中,库文件的前缀是
lib
,动态库文件的后缀是.so
,而静态库文件的后缀是
.a;所以,意味着最终生成的库文件对应的名字会自动添加上前缀和后缀。第二个参数表示库文件对应的源文件
本例中,
add_library
命令生成了一个静态库文件
liblibhello.a
,如果要生成动态库文件,可以这样做:
add_library(libhello SHARED hello.c) #生成动态库文件
add_library(libhello STATIC hello.c) #生成静态库文件
target_link_libraries
命令为目标指定依赖库,在本例中,
hello.c
被编译为库文件,并将其链接进
hello
程序
示例四:将源文件组织到不同的目录
上面的示例中,我们已经加入了多个源文件,但是这些源文件都是放在同一个目录下,这样还是不太正规,我们应该将这些源文件按照类型、功能、模块给它们放置到不同的目录下,于是将工程源码进行了整理,当前目录结构如下所示:
├──
build #build 目录
├──
CMakeLists.txt
├──
libhello
│ ├──
CMakeLists.txt
│ ├──
hello.c
│ └──
hello.h
└──
src
├──
CMakeLists.txt
└──
main.c
在工程目录下,我们创建了
src
和
libhello
目录,并将
hello.c
和
hello.h
文件移动到
libhello
目录下,将main.c 文件移动到
src
目录下,并且在顶层目录、
libhello
目录以及
src
目录下都有一个
CMakeLists.txt
文件。 CMakeLists.txt 文件的数量从
1
个一下变成了
3
个,顿时感觉到有点触不及防!还好每一个都不复杂!我们来看看每一个 CMakeLists.txt
文件的内容。
顶层
CMakeLists.txt
中使用了
add_subdirectory
命令,该命令告诉
cmake
去子目录中寻找新的
CMakeLists.txt
文件并解析它;
而在
src
的
CMakeList.txt
文件中,新增加了
include_directories
命令用来指明头文件所在的路径,并且使用到了 PROJECT_SOURCE_DIR
变量,该变量指向了一个路径,从命名上可知,
该变量表示工程源码的目录。
和前面一样,进入到
build
目录下进行构建、编译,最终会得到可执行文件
hello
(
build/src/hello
)和库文件 libhello.a
(
build/libhello/libhello.a
)
├──
build
│ ├──
libhello
│ │ └──
libhello.a
│ └──
src
│
└──
hello
├──
CMakeLists.txt
├──
libhello
│ ├──
CMakeLists.txt
│ ├──
hello.c
│ └──
hello.h
└──
src
├──
CMakeLists.txt
└──
main.c
示例五:将生成的可执行文件和库文件放置到单独的目录下
前面还有一点不爽,在默认情况下,
make
编译生成的可执行文件和库文件会与
cmake
命令产生的中间文件(CMakeCache.txt
、
CmakeFiles
、
cmake_install.cmake
以及
Makefile
等)混在一起,也就是它们在同一个目录下;如果我想让可执行文件单独放置在 bin
目录下,而库文件单独放置在
lib
目录下,就像下面这样:
├──
build
├──
lib
│ └──
libhello.a
└──
bin
└──
hello
将库文件存放在
build
目录下的
lib
目录中,而将可执行文件存放在
build
目录下的
bin
目录中,这个时候又该怎么做呢?这个时候我们可以通过两个变量来实现,将 src
目录下的
CMakeList.txt
文件进行修改, 如下所示:
include_directories(${PROJECT_SOURCE_DIR}/libhello)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
add_executable(hello main.c)
target_link_libraries(hello libhello)
然后再对 libhello 目录下的 CMakeList.txt 文件进行修改,如下所示:
set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
add_library(libhello hello.c)
set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")
修改完成之后,再次按照步骤对工程进行构建、编译,此时便会按照我们的要求将生成的可执行文件 hello 放置在 build/bin 目录下、库文件 libhello.a 放置在 build/lib 目录下。最终的目录结构就如下所示:
├──
build
│ ├──
bin
│ │ └──
hello
│ └──
lib
│
└──
libhello.a
├──
CMakeLists.txt
├──
libhello
│ ├──
CMakeLists.txt
│ ├──
hello.c
│ └──
hello.h
└──
src
├──
CMakeLists.txt
└──
main.c
其实实现这个需求非常简单,通过对
LIBRARY_OUTPUT_PATH
和
EXECUTABLE_OUTPUT_PATH 变 量 进 行 设 置 即 可 完 成 ; EXECUTABLE_OUTPUT_PATH
变量控制可执行文件的输出路径,而LIBRARY_OUTPUT_PATH 变量控制库文件的输出路径。