CMake学习笔记

该笔记参考这个博客

待查询名词

  • Everest系统
  • KDE4

CMake简介

CMake是一个跨平台的编译工具,CMake 的核心是读取一个容易理解的文本文件CMakeLists.txt,开发者可以往里面添加自己的源码目录,并把CMakeLists.txt这个文件放在源代码所在的目录中。通过运行CMake命令时,它会寻找这个文件,根据里面的内容生成标准的 Makefiles(UNIX 平台专用)或是利用命令行开关生成 XCode 项目文件(用于构建 OS X 系统上 XCode 开发工具所面向的 Mac 程序),甚至还能通过您的源代码生成 MSVC 项目。

CMake特点

  • 跨平台,并可生成native编译配置文件,在Linux/Unix平台,生成makefile,在苹果平台,可以生成xcode,在Windows平台,可以生成MSVC的工程文件。

  • 能够管理大型项目,KDE4就是最好的证明。

  • 简化编译构建过程和编译过程。Cmake的工具链非常简单:CMake+make。

  • 高效虑,按照KDE官方说法,CMake构建KDE4的kdelibs要比使用autotools来构建KDE3.5.6的kdelibs快40%,主要是因为 Cmake在工具链中没有libtool。

  • 可扩展,可以为CMake编写特定功能的模块,扩充CMake功能。

大佬给的意见

  • 如果你没有实际的项目需求,那么看到这里就可以停下来了,因为CMake的学习过程就是实践过程,没有实践,读的再多几天后也会忘记。

  • 如果你的工程只有几个文件,直接编写Makefile是最好的选择。

  • 如果使用的是C/C++/Java之外的语言,请不要使用CMake(至少目前是这样)

  • 如果你使用的语言有非常完备的构建体系,比如java的ant,也不需要学习CMake,虽然有成功的例子,比如QT4.3的csharp绑定qyoto。

  • 如果项目已经采用了非常完备的工程管理工具,并且不存在维护问题,没有必要迁移到CMake

  • 如果仅仅使用qt编程,没有必要使用CMake,因为qmake管理Qt工程的专业性和自动化程度比CMake要高很多。

CMake入门使用

创建空目录作为工作空间

1
2
$ mkdir -p backup/cmake/t1
$ cd backup/cmake/t1

创建源代码文件main.c并编写代码

1
2
3
4
5
#include<stdio.h>
int main(){
printf("hello cmake\n");
return 0;
}

创建CMakeLists.txt并编写指令 注意文件名称以及大小写要一致

1
2
3
4
5
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})

执行cmake命令

1
cmake .

执行完毕你的工作目录中会出现makefile

执行make命令,执行完这个命令你会找到编译好的可执行文件,名为hello

1
make

执行文件

1
./hello

更好一点的工程

本小节的任务是让前面的Hello World更像一个工程,我们需要作的是:

  • 为工程添加一个子目录src,用来放置工程源代码;
  • 添加一个子目录doc,用来放置这个工程的文档hello.txt
  • 在工程目录添加文本文件COPYRIGHT, README;
  • 在工程目录添加一个runhello.sh脚本,用来调用hello二进制
  • 将构建后的目标文件放入构建目录的bin子目录;
  • 最终安装这些文件:将hello二进制与runhello.sh安装至/usr/bin,将doc目录的内容以及COPYRIGHT/README安装到/usr/share/doc/cmake/t2

常用指令

PROJECT指令

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

指令定义了工程的名称,并可指定工程支持的语言,支持的语言列表是可以忽略的

使用这个指令会隐式的定义两个cmake变量:<projectname>_BINARY_DIR 以及 <projectname>_SOURCE_DIR,例如上一节中的HELLO_BINARY_DIR与HELLO_SOURCE_DIR

<projectname>_BINARY_DIR指向文件的编译目录,即你运行cmake命令的那个目录,<projectname>_SOURCE_DIR指向文件的源代码目录,即/backup/cmake/t1

CMake分为内部构筑与外部构筑;内部构筑即在工程源代码的根目录进行构筑,在此模式下<projectname>_BINARY_DIR<projectname>_SOURCE_DIR指向的地址是相同的,外部构筑通常我们会创建一个名为build的目录,在此目录中进行构筑,此模式下上述的两个变量指向的地址是不相同的

同时cmake系统也帮助我们预定义了 PROJECT_BINARY_DIRPROJECT_SOURCE_DIR 变量,他们的值分别与<projectname>_BINARY_DIR<projectname>_SOURCE_DIR一致;为了统一起见,建议以后直接使用 PROJECT_BINARY_DIRPROJECT_SOURCE_DIR,即使修改了工程名称,也不会影响这两个变量。如果使用了<projectname>_SOURCE_DIR,修改工程名称后,需要同时修改这些变量

SET指令

1
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])

目前只需要了解SET指令可以用来显式的定义变量即可。比如我们用到的是SET(SRC_LIST main.c),如果有多个源文件,也可以定义成SET(SRC_LIST main.c t1.c t2.c)

MESSAGE指令

1
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指令

1
ADD_EXECUTABLE(hello ${SRC_LIST})

定义了这个工程会生成一个文件名为hello的可执行文件,相关的源文件是SRC_LIST中定义的源文件列表, 上节你也可以直接写成ADD_EXECUTABLE(hello main.c)

在本例我们使用了${}来引用变量,这是CMake的变量应用方式,但是有一些例外,比如在IF控制语句,变量是直接使用变量名引用,而不需要${}。如果使用了${}去应用变量,其实IF会去判断名为${}所代表的值的变量,那当然是不存在的了。

清除工程

1
make clean

ADD_SUBDIRECTORY指令

在工程中每创建一个目录都需要在该目录中创建一个CMakeLists.txt,并且为了让CMake将这些目录中的文件进行构筑,需要使用ADD_SUBDIRECTORY指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建src目录
mkdir src
mv main.c src

# 现在的工程看起来是这个样子:一个子目录src,一个CMakeLists.txt
# 需要为任何子目录建立一个CMakeLists.txt,进入子目录src,编写CMakeLists.txt如下:

ADD_EXECUTABLE(hello main.c)

# 将t2工程根目录的CMakeLists.txt修改为:

PROJECT(HELLO)
ADD_SUBDIRECTORY(src bin)

#然后建立build目录,进入build目录进行外部编译。
cmake ..
make

1
ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

这个指令用于向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制存放的位置。EXCLUDE_FROM_ALL参数的含义是将这个目录从编译过程中排除,比如工程的example就需要工程构建完成后,再进入example目录单独进行构建(当然,你也可以通过定义依赖来解决此类问题)

上面的例子定义了将src子目录加入工程,并指定编译输出(包含编译中间结果)路径为bin目录。如果不进行bin目录的指定,那么编译结果(包括中间结果)都将存放在build/src目录(这个目录跟原有的src目录对应),指定bin目录后,相当于在编译时将src重命名为bin,所有的中间结果和目标二进制都将存放在bin目录。这里需要提一下的是SUBDIRS指令,使用方法是:SUBDIRS(dir1 dir2…),但是这个指令已经不推荐使用。它可以一次添加多个子目录,并且,即使外部编译,子目录体系仍然会被保存。如果我们在上面的例子中将ADD_SUBDIRECTORY (src bin)修改为SUBDIRS(src)。那么在build目录中将出现一个src目录,生成的目标代码hello将存放在src目录中

修改目标二进制文件输出位置

1
2
SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

上面两个指令分别定义了可执行二进制的输出路径为build/bin和库的输出路径为build/lib,目前位置我们没有涉及到共享库和静态库的构建

这两条指令写在工程的CMakeLists.txt还是src目录下的CMakeLists.txt?一个简单的原则,在哪里ADD_EXECUTABLE或ADD_LIBRARY且需要改变目标存放路径,就在哪里加入上述的定义

在这个例子里,当然就是指src下的CMakeLists.txt了

INSTALL命令

这里需要引入一个CMake指令INSTALL和一个变量DCMAKE_INSTALL_PREFIX

DCMAKE_INSTALL_PREFIX变量类似于configure脚本的–prefix,常见的使用方法看起来是这个样子:
cmake -DCMAKE_INSTALL_PREFIX=/usr .,DCMAKE_INSTALL_PREFIX的默认定义是/usr/local

INSTALL指令用于定义安装规则,安装的内容可以包括目标二进制、动态库、静态库以及文件、目录、脚本等。INSTALL指令包含了各种安装类型,接下来分开解释:

安装目标文件

1
2
3
4
5
6
7
8
9
INSTALL(TARGETS targets...
[[ARCHIVE|LIBRARY|RUNTIME]
[DESTINATION <dir>]
[PERMISSIONS permissions...]
[CONFIGURATIONS
[Debug|Release|...]]
[COMPONENT <component>]
[OPTIONAL]
] [...])

参数中的TARGETS后面跟的就是我们通过ADD_EXECUTABLE或者ADD_LIBRARY定义的目标文件,可能是可执行二进制、动态库、静态库。目标类型也就相对应的有三种,ARCHIVE特指静态库,LIBRARY特指动态库,RUNTIME特指可执行目标二进制。

DESTINATION定义了安装的路径,如果路径以/开头,那么指的是绝对路径,这时候CMAKE_INSTALL_PREFIX其实就无效了。如果你希望使用CMAKE_INSTALL_PREFIX来定义安装路径,就要写成相对路径,即不要以/开头,那么安装后的路径就是${CMAKE_INSTALL_PREFIX}/<DESTINATION定义的路径>

举个简单的例子

1
2
3
4
5
INSTALL(TARGETS myrun mylib mystaticlib
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION libstatic
)

安装普通文件

1
2
3
4
5
INSTALL(FILES files... DESTINATION <dir>
[PERMISSIONS permissions...]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[RENAME <name>] [OPTIONAL])

安装一般文件,并可以指定访问权限,文件名是此指令所在路径下的相对路径。如果默认不定义权限PERMISSIONS,安装后的权限为:OWNER_WRITE, OWNER_READ, GROUP_READ,和WORLD_READ,即644权限

举个简单的例子

1
INSTALL(FILES COPYRIGHT README DESTINATION share/doc/cmake/t2)

安装非目标文件的可执行程序(脚本)

1
2
3
4
5
INSTALL(PROGRAMS files... DESTINATION <dir>
[PERMISSIONS permissions...]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[RENAME <name>] [OPTIONAL])

跟上面的FILES指令使用方法一样,唯一的不同是安装后权限为:OWNER_EXECUTE, GROUP_EXECUTE, 和WORLD_EXECUTE,即755权限

举个简单的例子

1
INSTALL(PROGRAMS runhello.sh DESTINATION bin)

安装目录

1
2
3
4
5
6
7
8
INSTALL(DIRECTORY dirs... DESTINATION <dir>
[FILE_PERMISSIONS permissions...]
[DIRECTORY_PERMISSIONS permissions...]
[USE_SOURCE_PERMISSIONS]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[[PATTERN <pattern> | REGEX <regex>]
[EXCLUDE] [PERMISSIONS permissions...]] [...])

DIRECTORY后面连接的是所在Source目录的相对路径,但务必注意:abc和abc/有很大的区别。如果目录名不以/结尾,那么这个目录将被安装为目标路径下的abc,如果目录名以/结尾,代表将这个目录中的内容安装到目标路径,但不包括这个目录本身。PATTERN用于使用正则表达式进行过滤,PERMISSIONS用于指定PATTERN过滤后的文件权限

举个简单的例子

1
2
3
4
5
INSTALL(DIRECTORY icons scripts/ DESTINATION share/myproj
PATTERN "CVS" EXCLUDE
PATTERN "scripts/*"
PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ
GROUP_EXECUTE GROUP_READ)

将icons目录安装到 <prefix>/share/myproj,将scripts/中的内容安装到<prefix>/share/myproj不包含目录名为CVS的目录,对于scripts/*文件指定权限为 OWNER_EXECUTE OWNER_WRITE OWNER_READ ROUP_EXECUTE GROUP_READ

ADD_LIBRARY指令

前置工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 在lib目录下建立两个源文件hello.c与hello.h
# hello.c内容如下:

#include "hello.h"
void HelloFunc()
{
printf("Hello World\n");
}

# hello.h内容如下:

#ifndef HELLO_H
#define HELLO_H
#include <stdio.h>
void HelloFunc();
#endif
`

该指令用于制定构筑动态库或者静态库

1
ADD_LIBRARY(libname [SHARED|STATIC|MODULE][EXCLUDE_FROM_ALL]source1 source2 ... sourceN)

你不需要写全libhello.so,只需要填写hello即可,CMake系统会自动为你生成libhello.X。类型有三种:

  • SHARED,动态库(扩展名为.so)
  • STATIC,静态库(扩展名为.a)
  • MODULE,在使用dyld的系统有效,如果不支持dyld,则被当作SHARED对待。
  • EXCLUDE_FROM_ALL 参数的意思是这个库不会被默认构建,除非有其他的组件依赖或者手工构建。
    1
    2
    3
    4
    5
    SET(LIBHELLO_SRC hello.c)
    ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})
    ```

    接下来创建静态库

ADD_LIBRARY(hello_static STATIC ${LIBHELLO_SRC})

1
在CMake中因为hello作为一个target是不能重名的,所以静态库的名称不能是hello了,此时构筑出来的是一个名为hello的动态库以及一个叫做hello_static的静态库,这不是我们目的,所以可以通过如下命令修改文件的输出名称

SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME “hello”)

1
但是在CMake在构建一个新的target时,会尝试清理掉其他使用这个名字的库,在构建libhello.a时,就会清理掉libhello.so,为了回避这个问题,比如再次使用SET_TARGET_PROPERTIES定义

SET_TARGET_PROPERTIES(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1)
SET_TARGET_PROPERTIES(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

综上完整的构筑动态库静态的代码示例为
```shell
SET(LIBHELLO_SRC hello.c)
ADD_LIBRARY(hellow SHARED ${LIBHELLO_SRC})
ADD_LIBRARY(hellow_static STATIC ${LIBHELLO_SRC})

SET_TARGET_PROPERTIES(hellow_static PROPERTIES OUTPUT_NAME "hellow")
SET_TARGET_PROPERTIES(hellow PROPERTIES CLEAN_DIRECT_OUTPUT 1)
SET_TARGET_PROPERTIES(hellow_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)

INSTALL(TARGETS hellow hellow_static
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)

INSTALL(FILES hello.h DESTINATION include/hello)

在工程中引用动态库静态库

引入头文件搜索路径

如果你引用的第三方库头文件已经放入到/usr/include中那么不需要再做本操作

1
INCLUDE_DIRECTORIES([AFTER|BEFORE] [SYSTEM] dir1 dir2 ...)

这条指令可以用来向工程添加多个特定的头文件搜索路径,路径之间用空格分割,如果路径中包含了空格,可以使用双引号将它括起来,默认的行为是追加到当前的头文件搜索路径的后面,你可以通过两种方式来进行控制搜索路径添加的方式:

  • CMAKE_INCLUDE_DIRECTORIES_BEFORE,通过SET这个cmake变量为on,可以将添加的头文件搜索路径放在已有路径的前面
  • 通过AFTER或者BEFORE参数,也可以控制是追加还是置前。现在我们在src/CMakeLists.txt中添加一个头文件搜索路径,方式很简单,加入:INCLUDE_DIRECTORIES(/usr/include/hello)

引入动态库

需要使用LINK_DIRECTORIESTARGET_LINK_LIBRARIES两个指令

1
LINK_DIRECTORIES(directory1 directory2 ...)

这个指令非常简单,添加非标准的共享库搜索路径,比如,在工程内部同时存在共享库和可执行二进制,在编译时就需要指定一下这些共享库的路径

1
TARGET_LINK_LIBRARIES(target library1<debug | optimized> library2...)

这个指令可以用来为target添加需要链接的共享库,TARGET_LINK_LIBRARIES(main hello)也可以写为TARGET_LINK_LIBRARIES(main libhello.so)

引入静态库

1
TARGET_LINK_LIBRARIES(main libhello.a)

其他指令

以下内容出现的指令用小写~

aux_source_directory

1
aux_source_directory(<dir> <variable>)

该命令会查找指定目录下的所有源文件,然后将结果存进指定变量名

1
2
3
# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)

本文结束 感谢阅读
0%