2019-09-22-c-cmake-基本例子及目录结构

我想念泉州的夏天了

CMake

cmake是什么

CMake is an open-source, cross-platform family of tools designed to build, test and package software. CMake is used to control the software compilation process using simple platform and compiler independent configuration files, and generate native makefiles and workspaces that can be used in the compiler environment of your choice. –来自官网的解说

翻译成白话文就是

CMake是一个以编译、测试、打包软件为目标的开源、跨平台的工具集,CMake使用简单的与平台和编译器无关的配置文件来控制软件的编译过程,并生成您选择的编译器环境中可使用的本地makefile工作区

why cmake?

另外有一个autotools工具集,也是相同的作用,当前github上面有许多项目使用它,但它比CMake复杂。

master-cmake这本书,第一章就解释为什么用cmake,列举cmake的各项优点以及和autotools的比较。

这里有这个段落的中文翻译,有兴趣可以看看–为何要使用cmake中文翻译参考

一个简单的例子

当前所在路径

test01# pwd
/tmp/c/2019-09-12-cmaketest/test01

当前目录中的文件/文件夹

test01# tree -F
.
└── helloworld.cpp

0 directories, 1 file

helloworld.cpp内容很简单,就打印一个hello world

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

int main(int argc, char * args[]) {
  printf("hello world\n");
}

我们想要编译这个文件,可以

test01# gcc -o helloworld helloworld.cpp

此时生成了一个可执行文件helloworld

test01# tree -F
.
├── helloworld*
└── helloworld.cpp

0 directories, 2 files

我们现在可以愉快的运行这个helloworld

test01# ./helloworld 
hello world

将上面例子改造成CMake方式编译

当前所在路径

test02# pwd
/tmp/c/2019-09-12-cmaketest/test02

当前目录中的文件/文件夹

test02# tree -F
.
├── CMakeLists.txt
└── helloworld.cpp

0 directories, 2 files

其中helloworld.cpp还是test01中的老样子,CMakeLists.txt则是最开始提到的–CMake使用的与平台和编译器无关的配置文件

CMakeLists.txt的内容,配置项含义稍后解释。

test02# cat CMakeLists.txt 
# 所需CMAKE最低版本,若当前使用版本低于此最低版本,则报错退出
CMAKE_MINIMUM_REQUIRED (VERSION 2.6)
# 项目名称
PROJECT (HELLOWORLD)
# 打印信息
MESSAGE (STATUS "this is BINARY dir " ${HELLOWORLD_BINARY_DIR})
MESSAGE (STATUS "this is SOURCE dir " ${HELLOWORLD_SOURCE_DIR})
# 生成可执行文件hellworld
ADD_EXECUTABLE (helloworld helloworld.cpp)

现在我们有了配置文件和项目源文件,可以使用cmake来编译一下了

test02# cmake .
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.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
-- this is BINARY dir /tmp/c/2019-09-12-cmaketest/test02
-- this is SOURCE dir /tmp/c/2019-09-12-cmaketest/test02
-- Configuring done
-- Generating done
-- Build files have been written to: /tmp/c/2019-09-12-cmaketest/test02

再看一下当前目录下的第一层文件/文件夹,生成了一堆东西,除开原来有的CMakeLists.txt和helloworld.cpp,生成了CMakeCache.txt、CMakeFiles文件夹(其中包含文件没有显示)、cmake_install.cmake、Makefile,这些东西就是前面说的–生成您选择的编译器环境中可使用的本地makefile工作区

test02# tree -F -L 1
.
├── CMakeCache.txt
├── CMakeFiles/
├── cmake_install.cmake
├── CMakeLists.txt
├── helloworld.cpp
└── Makefile

1 directory, 5 files

有了这些东西,我们可以使用make命令生成可执行文件了

test02# make
Scanning dependencies of target helloworld
[ 50%] Building CXX object CMakeFiles/helloworld.dir/helloworld.cpp.o
[100%] Linking CXX executable helloworld
[100%] Built target helloworld

当前目录下的第一层文件/文件夹

test02# tree -F -L 1
.
├── CMakeCache.txt
├── CMakeFiles/
├── cmake_install.cmake
├── CMakeLists.txt
├── helloworld*
├── helloworld.cpp
└── Makefile

1 directory, 6 files

多出了一个可执行文件hellworld,我们这时候就可以运行这个helloworld了。

test02# ./helloworld 
hello world

接下来解释一下上面的一些东西

why cmake?

简单的例子里面,用gcc编译一下就可以愉快的运行了

test01# gcc -o helloworld helloworld.cpp

而cmake却要编译一个CMakeLists.txt配置文件,然后cmake生成一堆东西,再make才能达到上面一句相同的作用,是不是让你怀疑为什么要用cmake了?和maven、ant等一些工具类似的,当项目非常简单的时候,不用工具手动编译还更快,但是项目复杂起来,几十个几百个源文件的时候,你就会体会到工具带来的好处了。

cmake 和 make 是什么东西?

看看cmake的帮助文档

test02# man cmake
...
DESCRIPTION
       The "cmake" executable is the CMake command-line interface.  It may be used to configure projects in scripts.  Project configura‐
       tion settings may be specified on the command line with the -D option.

       CMake is a cross-platform build system generator.  Projects specify their build process with platform-independent CMake listfiles
       included  in  each  directory  of a source tree with the name CMakeLists.txt.  Users build a project by using CMake to generate a
       build system for a native tool on their platform.
...

翻译之

“cmake”可执行文件是cmake命令行接口。它可以用于在脚本中配置项目。项目的配置项可以在命令行中使用-D选项指定。

CMake是一个跨平台的构建系统生成器。项目使用与平台无关的、在每个包含源文件的文件夹中的名为CMakeLists.txt指定编译过程。用户使用CMake生成一个项目在他们的平台上为本地工具构建系统。

再看看make的帮助文档

test02# man make
...
DESCRIPTION
       The  make  utility  will determine automatically which pieces of a large program need to be recompiled, and issue the commands to
       recompile them.  The manual describes the GNU implementation of make, which was written by Richard Stallman and  Roland  McGrath,
       and  is  currently maintained by Paul Smith.  Our examples show C programs, since they are very common, but you can use make with
       any programming language whose compiler can be run with a shell command.  In fact, make is not limited to programs.  You can  use
       it to describe any task where some files must be updated automatically from others whenever the others change.

       To  prepare  to use make, you must write a file called the makefile that describes the relationships among files in your program,
       and the states the commands for updating each file.  In a program, typically the executable file is updated  from  object  files,
       which are in turn made by compiling source files.

       Once a suitable makefile exists, each time you change some source files, this simple shell command:

              make

       suffices to perform all necessary recompilations.  The make program uses the makefile description and the last-modification times
       of the files to decide which of the files need to be updated.  For each of those files, it issues the commands  recorded  in  the
       makefile.

       make executes commands in the makefile to update one or more target names, where name is typically a program.  If no -f option is
       present, make will look for the makefiles GNUmakefile, makefile, and Makefile, in that order.

       Normally you should call your makefile either makefile or Makefile.  (We recommend Makefile because it appears  prominently  near
       the  beginning of a directory listing, right near other important files such as README.)  The first name checked, GNUmakefile, is
       not recommended for most makefiles.  You should use this name if you have a makefile that is specific to GNU make, and  will  not
       be understood by other versions of make.  If makefile is '-', the standard input is read.

       make  updates  a target if it depends on prerequisite files that have been modified since the target was last modified, or if the
       target does not exist.
...

翻译之

make程序将自动确定一个大型程序的哪些部分需要重新编译,并发出命令重新编译它们。手册描述了make的GNU实现,它是由Richard Stallman和Roland McGrath编写的,目前由保罗·史密斯维护。我们的示例展示了C程序,因为它们非常常见,但是您可以使用make with任何一种编程语言,其编译器可以用shell命令运行。事实上,make并不局限于程序。您可以使用它描述任何任务,其中一些文件必须在其他文件更改时从其他文件自动更新。

要准备使用make,您必须编写一个名为makefile的文件来描述程序中文件之间的关系,以及用于更新每个文件的命令。在程序中,通常可执行文件是从目标文件更新的,它们是通过编译源文件来实现的。

一旦一个合适的makefile存在,每次你改变一些源文件,这个简单的shell命令:

make

足以执行所有必要的重新编译。make程序使用makefile描述和最后修改时间来决定哪些文件需要更新,对于每个文件,它发出makefile中记录的命令。

make在makefile中执行命令来更新一个或多个目标名称,其中名称通常是一个程序。如果没有-f选项,现在,make将按照这个顺序查找以下的makefile文件:GNUmakefile、makefile和Makefile。

通常,您应该将makefile文件命名为makefile或Makefile。(我们推荐Makefile,因为它在显著位置附近目录列表的开头,紧挨着其他重要文件,如README。)选中的第一个名称GNUmakefile是不推荐大多数makefile。如果您有一个特定于GNU make的makefile,而不是will,那么您应该使用这个名称被make的其他版本所理解。如果makefile是’-‘,则读取标准输入。

make更新目标取决于自上次修改目标以来已修改的先决条件文件,或者目标不存在。

—分割线在此

从前面的文档可以看到make是不属于cmake的一部分的,cmake来生成make文件需要的Makefile文件,make再用Makefile文件生成可执行文件helloworld。

也就是说我们可以手动编辑Makefile文件,然后直接用make来运行也能生成helloworld,但是看看Makefile文件中的足有178行(去掉空行和注释行也还有很多行),是什么让我们望而却步 0.0 。

test02# wc -l Makefile 
178 Makefile

如果对cmake,make想要更多的了解,你们可以用man去看详细配置项及信息。

cmake生成了些啥?

再看看test02最后目录中的文件/文件夹

test02# tree -F -L 1
.
├── CMakeCache.txt
├── CMakeFiles/
├── cmake_install.cmake
├── CMakeLists.txt
├── helloworld*           可执行文件
├── helloworld.cpp        源文件
└── Makefile

1 directory, 6 files

CMakeCache.txt

CMakeCache.txt是运行cmake之后,cmake根据CMakeLists.txt生成的缓存文件,当第一次运行cmake之后,我们修改了CMakeLists.txt并且再次运行CMake,此时有可能有一些配置项等是不起作用的(有些是起作用),因为他们缓存在CMakeCache.txt,而每次运行cmake的时候是不会删除CMakeCache.txt后再生成一份的,而是根据cmake自己的原则优先读取CMakeCache.txt中的配置项。

我们可以通过删除CMakeCache.txt文件然后再运行cmake来生成新的一份CMakeCache.txt。

如果只是修改某个配置,那么也可以直接修改CMakeCache.txt。

CMakeFiles/

这个文件夹下放了一堆东西,中间产物。

cmake_install.cmake

看名字就知道它是指定安装文件的信息。

CMakeLists.txt

cmake 和 make 是什么东西?所说,是cmake使用的配置文件,每个包含源文件(不仅仅是源文件)都需要包含一个CMakeLists.txt文件

Makefile

cmake 和 make 是什么东西?所说,这是make需要用的配置文件,其中命令什么的看一下大概了解就好,一般我们不需要手动编辑它。

配置文件中

让我们来好好看看配置文件

test02# cat CMakeLists.txt 
CMAKE_MINIMUM_REQUIRED (VERSION 2.6)
PROJECT (HELLOWORLD)
MESSAGE (STATUS "this is BINARY dir " ${HELLOWORLD_BINARY_DIR})
MESSAGE (STATUS "this is SOURCE dir " ${HELLOWORLD_SOURCE_DIR})
ADD_EXECUTABLE (helloworld helloworld.cpp)

cmake_minimum_required 需要的cmake最小版本

如果当前系统的cmake版本低于这个版本,那么将会直接退出。

message 打印消息

MESSAGE([SEND_ERROR | STATUS | FATAL_ERROR] "message to display" ...)

这个指令用于向终端输出用户定义的信息,包含了三种类型:

  • SEND_ERROR,产生错误,生成过程被跳过。
  • SATUS,输出前缀为 —-的信息。
  • FATAL_ERROR,立即终止所有 cmake 过程.

在这里使用的是 STATUS 信息输出,演示了由 PROJECT 指令定义的两个隐式变HELLOWORLD_SOURCE_DIRHELLOWORLD_SOURCE_DIR

MESSAGE (STATUS "this is BINARY dir " ${HELLOWORLD_BINARY_DIR})
MESSAGE (STATUS "this is SOURCE dir " ${HELLOWORLD_SOURCE_DIR})

在cmake的时候可以看到对应输出的消息

test02# cmake .
...
-- this is BINARY dir /tmp/c/2019-09-12-cmaketest/test02
-- this is SOURCE dir /tmp/c/2019-09-12-cmaketest/test02
...

project 项目名称

你可以用这个指令定义工程名称,并可指定工程支持的语言,支持的语言列表是可以忽略的。(当我发现它还支持Java的时候,我的内心是拒绝的,因为maven很好用 :) )

PROJECT(projectname [CXX] [C] [Java])

默认情况表示支持所有语言。这个指令隐式的定义了两个cmake变量: <projectname>_BINARY_DIR 以及<projectname>_SOURCE_DIR,这里就是 HELLOWORLD_BINARY_DIRHELLOWORLD_SOURCE_DIR,因为采用的是内部构建,两个变量目前指的都是工程所在路径/tmp/c/2019-09-12-cmaketest/test02,所以上面cmake的时候打印的都是这个路径。后面我们会讲到外部构建,两者所指代的内容会有所不同。

同时cmake系统也帮助我们预定义了 PROJECT_BINARY_DIRPROJECT_SOURCE_DIR 变量,他们的值分别跟 HELLO_BINARY_DIRHELLO_SOURCE_DIR 一致。

为了统一起见,建议以后直接使用 PROJECT_BINARY_DIRPROJECT_SOURCE_DIR,即使修改了工程名称,也不会影响这两个变量。如果使用了 <projectname>_SOURCE_DIR,修改工程名称后,需要同时修改这些变量。

add_executable 生成可执行的文件

ADD_EXECUTABLE (helloworld hellworld.cpp)

定义了这个工程会生成一个文件名为 helloworld 的可执行文件,相关的源文件是hellworld.cpp。

基本语法规则

  • 1,变量使用${}方式取值,但是在IF控制语句中是直接使用变量名

如前面想取${HELLOWORLD_BINARY_DIR}的值,如下

MESSAGE (STATUS "this is BINARY dir " ${HELLOWORLD_BINARY_DIR})
  • 2,指令(参数 1 参数 2…)

参数使用括弧括起,参数之间使用空格或分号分开。 以上面的ADD_EXECUTABLE指令为例,如果存在另外一个 func.cpp 源文件,就要写成:

以空格分割的形式

ADD_EXECUTABLE(hello hellworld.cpp func.cpp)

或者 以分号分割的形式

ADD_EXECUTABLE(hello hellworld.cpp;func.cpp) 
  • 3,指令是大小写无关的,参数和变量是大小写相关的。但推荐你全部使用大写指令。

内部构建和外部构建

test02# tree -F -L 1
.
├── CMakeCache.txt
├── CMakeFiles/
├── cmake_install.cmake
├── CMakeLists.txt
├── helloworld*           可执行文件
├── helloworld.cpp        源文件
└── Makefile

1 directory, 6 files

在上面这个例子,生成的一堆中间文件和源文件(helloworld.cpp和CMakeLists.txt)混在一起,这个就很讨厌了,比如我们同步到代码库中的代码一般不会包含这些中间文件,要是一个一个去限制,那够累的。

之前写过java使用maven来构建的就知道,maven是将所有的的中间文件放到target目录下,每次使用mvn clean就会删除这个文件夹中的所有中间文件,这样既没有和源码混在一起,也让管理中间文件变得容易。

在cmake中,可以使用make clean来清除构建结果(只把可执行文件删了),但是cmake不支持make distclean。因为 CMakeLists.txt 可以执行脚本并通过脚本生成一些临时文件,但是却没有办法来跟踪这些临时文件到底是哪些,因此,没有办法提供一个可靠的make distclean方案,无法删除其它一些中间文件。

Some build trees created with GNU autotools have a “make distclean” target that cleans the build and also removes Makefiles and other parts of the generated build system. CMake does not generate a “make distclean” target because CMakeLists.txt files can run scripts and arbitrary commands; CMake has no way of tracking exactly which files are generated as part of runningCMake. Providing a distclean target would give users the false impression that it would work as expected. (CMake does generate a “make clean” target to remove files generated by the compiler and linker.)

A “make distclean” target is only necessary if the user performs an in-source build. CMake supports in-source builds, but we strongly encourage users to adopt the notion of an out-of-source build. Using a build tree that is separate from the source tree will prevent CMake from generating any files in the source tree. Because CMake does not change the source tree, there is no need for a distclean target. One can start a fresh build by deleting the build tree or creating a separate build tree. – cmake官方

同时,还有另外一个非常重要的提示,就是:我们刚才进行的是内部构建(in-source build),而 cmake 强烈推荐的是外部构建(out-of-source build)。 就是把那一堆中间文件放到一个文件夹下便于管理。

我们创建一个build文件夹(至于为什么叫build,和java是target一样吧,约定俗成了吧。约定优于配置,我喜欢这句话),在这个文件夹中进行构建(而不是在源代码目录下),那么所有的中间文件都会在这个build文件夹下。

关于build文件夹,有两种存放方式,第一种是把build文件夹放在项目的目录下,如下所示

dir/
  project01/
    helloworld.cpp
    CMakeLists.txt
    build/
  project02/
    helloworld.cpp
    CMakeLists.txt
    build/

第二种是在项目的目录外(推荐这种),这种情况下所有项目共用一个build目录,如下所示

dir/
  build/
  project01/
    helloworld.cpp
    CMakeLists.txt
  project02/
    helloworld.cpp
    CMakeLists.txt

当然不一定非要共用一个build目录,你可以取不同的名字,如下所示

dir/
  build01/
  build02/
  project01/
    helloworld.cpp
    CMakeLists.txt
  project02/
    helloworld.cpp
    CMakeLists.txt

一般我用的第二种。现在使用第一种来演示一下,因为这种演示看的比较清楚。其它的都是一个意思。

当前所在路径

test03# pwd
/tmp/c/2019-09-12-cmaketest/test03

当前目录下的文件/文件夹

/test03# tree -F
.
├── build/
├── CMakeLists.txt
└── helloworld.cpp

1 directory, 2 files

CMakeLists.txt文件和helloworld.cpp文件和test02是一样的,新建了一个build目录。

现在让我们进入build目录进行编译

test03# cd build/

test03/build# cmake ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.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
-- this is BINARY dir /tmp/c/2019-09-12-cmaketest/test03/build
-- this is SOURCE dir /tmp/c/2019-09-12-cmaketest/test03
-- Configuring done
-- Generating done
-- Build files have been written to: /tmp/c/2019-09-12-cmaketest/test03/build

可以看到此时两个路径之间的区别了,源代码目录仍然是/tmp/c/2019-09-12-cmaketest/test03,而构建目录是/tmp/c/2019-09-12-cmaketest/test03/build

-- this is BINARY dir /tmp/c/2019-09-12-cmaketest/test03/build
-- this is SOURCE dir /tmp/c/2019-09-12-cmaketest/test03

生成可执行文件并运行

test03/build# make
Scanning dependencies of target helloworld
[ 50%] Building CXX object CMakeFiles/helloworld.dir/helloworld.cpp.o
[100%] Linking CXX executable helloworld
[100%] Built target helloworld

root@iZbp1ccq9xttkzab5d3fc9Z:/tmp/c/2019-09-12-cmaketest/test03/build# ./helloworld 
hello world

返回上一层目录看看

test03/build# cd ..

root@iZbp1ccq9xttkzab5d3fc9Z:/tmp/c/2019-09-12-cmaketest/test03# tree -F -L 2
.
├── build/
│   ├── CMakeCache.txt
│   ├── CMakeFiles/
│   ├── cmake_install.cmake
│   ├── helloworld*
│   └── Makefile
├── CMakeLists.txt
└── helloworld.cpp

2 directories, 6 files
root@iZbp1ccq9xttkzab5d3fc9Z:/tmp/c/2019-09-12-cmaketest/test03# 

可以看到现在,所有的中间文件都已经在build目录中(整个世界都干净了),如果我们想要删除所有中间文件,只需要删除build目录下所有的文件即可。

推荐使用外部构建的方式,因为当你的源文件一多,再和这些中间文件凑在一起,想分开都不容易啊。

参考