CMake 是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程)。他能够输出各种各样 Makefile 或者 project 文件,能测试编译器所支持的 C++ 特性,类似 UNIX 下的 automake。
说在前头的个人的一些想法 (《CMake Practice》 也这么说 ) 和建议吧:
①. CMake 的学习过程是实践过程,没有实践,读的再多几天后也会忘记。所以,最好是你现在有实际的项目需求(没有的话,希望这个日后还可以作为参考),另外,文章中的实例请你必须亲自运行验证一下。
②. 如果你的工程只有几个文件,直接编写 Makefile 是个可行的选择,不过考虑到移植性,比如究竟是gcc呢?g++呢?还是什么其他的呢?还是使用 CMake 吧。
③. 如果使用的是 C/C++/Java 之外的语言,请不要使用 CMake(目前还不支持)。
④. 如果你使用的语言有非常完备的构建体系,比如 Java 的 Ant,也不需要学习 CMake。
CMake 简介
先来说说 Unix 下的 automake 吧。
某天我要安装某个开源项目 tinySLAM,结果,人家用的是 automake,结果就悲剧了。
README 提示先运行他的某个脚本 ./bootstrap,其实里面就是一堆 automake 的命令,内容如下。然后就是典型的 ./configure,make, make install。
1 | libtoolize |
结果,第一步就报错了…
搞到最后,才发现,automake 死板的要求你必须有某些文件,空的也行,不然不让你构建…所以,只能很无奈的表示屈服,通过下面的命令,问题是解决了。
1 | $ touch NEWS README AUTHORS ChangeLog |
所以说,automake 实在不行啦,确实需要 CMake 来拯救。通过上面的例子,也是想说明一下,了解包括 automake 在内的构建套件,与我们的日常开发息息相关。因为开源,源代码都有了,那就需要你本地构建、安装,这就需要构建套件的支持。所以很多源码都会提**供构建套件需要的配置文件(如上面的 ./bootstrap 和 ./configure),而目前比较受欢迎,使用最广的(你可以自己看看 github…),便是 CMake,所以你会在很多源代码中看到 CMakeLists.txt 的踪影。
因此,学习 CMake,熟悉 CMake 的基本语法,有助于我们了解源代码的依赖关系,更好的阅读开源工程;另外,更重要的,则是,源代码的本地构建、安装并不总是顺利的(如上面的例子),对 CMake 有一定的了解,能更好的解决我们在构建、安装过程中遇到的问题!
CMake 的组态档取名为 CMakeLists.txt。CMake 并不直接建构出最终的软件,而是产生标准的建构档(如 Unix 的 Makefile)。
CMake 是跨平台,可生成 native 编译配置文件,在 Linux/Unix平台,生成 Makefile,在苹果平台,生成 xcode,在 Windows 平台,可以生成 MSVC 工程文件。
CMake 的应用非常广泛!Qt 采用了 cmake 作为构建系统,openCV 采用 cmake 作为构建系统…
简单的 HelloWorld
我们所有的 CMake 练习均放在 ~/Workspace/cmake_ws/ 下,一般情况下,Ubuntu 都自带CMake ,Windows 下也可以安装 CMake。
1 | > cd ~/Workspace/ |
新建的工程源文件 main.c 和 CMake 文件 CMakeLists.txt 内容如下:
1 | // main.c |
注:
main.c 是一个再典型不过的 hello world。
1 | # CMakeLists.txt |
注:
上述是一个简单的 CMake 文件,CMAKE_MINIMUM_REQUIRED 指令用于检查系统 CMake 版本是否满足最低要求,PROJECT 指令通过名字定义工程,SET 用于设置 CMake 变量,另外通过 ${VAR_NAME} 的方式引用变量,MESSAGE 用于向终端输出信息,ADD_EXECUTABLE 则用于生成可执行二进制文件。 CMake 指令是大小写不敏感的,很多开源项目的 CMakeLists.txt 中,指令都是小写的。更多具体的语法如下:
1 | # 定义这个工程会生成一个文件名为 target 的可执行文件,相关的源文件是source1、source2...定义的源文件列表 |
开始构建:
所有的文件创建完成后,ex_01 目录中应该存在 main.c 和 CMakeLists.txt 两个文件,接下来我们来构建这个工程,在这个目录运行:
1 | > cd ~/Workspace/cmake_ws/ex_01/ |
“cmake .”,. 表示当前目录,运行后将会自动生成 CMakeFiles 文件夹,CMakeCache.txt,cmake_install.cmake 等文件,并且生成了 Makefile。下一步 “make” 则是去执行的 Makefile,生成我们的目标文件 hello。你可以通过 “make VERBOSE=1” 看到 make 构建的详细过程。最后通过 ./hello 即可运行目标二进制文件。完成构建后的工程源文件目录如下:
实际上,上面我们采用的是一种叫做 in-source build(内部构建) 的构建方式,顾名思义,这种方式直接在源代码中进行构建,构建过程的中间产物以及最终的目标文件都会混在一起,我们没办法将项目文件与其分开,更没办法做到自动删除这些中间文件和目标文件。
另外一种比较合理的构建方式叫做 out-of-source build(外部构建) ,这种方式单独在与源代码工程独立的目录下执行构建,保证源代码的纯洁性,更能实现中间文件和目标文件的快速删除。
1 | > cd ~/Workspace/cmake_ws/ex_01/ |
“cmake ..” 中的 “..” 表示 build 目录的父目录,即是我们的源代码工程目录,因为父目录存在我们需要的 CMakeLists.txt。这里要说明的是,你也可以在其他地方进行构建(“cmake
通过 外部构建 这种方式完成构建后的工程源文件目录如下,构建过程中的中间产物和目标文件都存放在 build 目录中,源文件并没有受其影响。通过删除该文件夹,即可实现中间文件快速删除。建议大家采用这种方式进行构建,后续的练习也均按照这种方式进行
更好一点的 HelloWorld
上述的工程还不够好,跟一般的项目工程还有很大区别,一般项目工程大概如下:
1 | . |
src,用来放置工程源代码,包括源文件、头文件以及该目录的 CMake 文件;doc,用来放置工程的说明文档;文本文件 COPYRIGHT 和 README 是有关 license 和如何使用的说明;构建后的目标文件将放在构建目录的 bin 子目录下;工程目录下添加一个 run<project_name>.sh
脚本,可以用来调用二进制文件,比如需要按照一定配置、顺序调用多个目标文件,才能完成整个项目的功能。
首先,建立该目录树,编写 src 目录下的工程源代码,包括 main.c 和该目录的 CMake 文件 CMakeLists.txt
1 | // ./src/main.c |
main.c 文件基本与第一个例子一样。
1 | # ./src/CMakeLists.txt |
ADD_EXECUTABLE 用于生成目标文件,INSTALL 命令则是跟 make install 安装相关,这里是安装目标文件。
接下来是工程根目录下的 CMakeLists.txt,该文件是整个工程的 CMake 文件,需要使用 src 目录下的 CMake 文件。
1 | # ./CMakeLists.txt |
ADD_SUBDIRECTORY 将整个工程的 CMake 文件与 src 工程源代码关联;INSTALL 用于安装,这里是安装脚本文件、普通文件和 doc 目录。ADD_TEST 和 ENABLE_TESTING 则与 “make test” 相关,具体的使用方法如下:
1 | # 这个指令用于向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制存放的位置。 |
构建:
构建过程基本与上一个例子一样,make 之后,生成的目标文件将保存在 ./build/bin/ 目录下,make test、make install 分别执行上述的 ADD_TEST 和 INSTALL 指令。
1 | > cd ~/Workspace/cmake_ws/ex_02/ |
前面说过,DESTINATION 定义了安装的路径,如果路径以 / 开头,那么指的是绝对路径;否则是相对路径 ${CMAKE_INSTALL_PREFIX}/<dir>
,默认的的安装路径前缀 ${CMAKE_INSTALL_PREFIX} 是 /usr/local,所以,采用默认的安装路径 make install 时,会因为权限问题而报错。
在执行 “cmake” 命令时可以指定参数 CMAKE_INSTALL_PREFIX,如下:
安装:
通过前面的分析可以看出,安装其实就是将目标文件(可能是二进制文件或者静态库)及其依赖的文件(可能是动态库),还有相关的说明文档等放在指定的目录下。
可能你会有这样的疑问:不就存放些文件嘛,干嘛说成安装这么高大上?
其实,我个人理解,称为 安装 主要是因为存放的路径比较特殊,如上面提到的 /usr/local,这些路径存在于 shell 的默认路径中。举个例子,某个可执行文件安装到 /usr/local 中,那他在任何一个terminal 中都能被运行,这不就像在 Windows 下,随便双击某个桌面图标,就能启动程序一样,把那个程序安装到系统上了。
CMake,库的创建、安装&使用
了解了二进制可执行文件的 CMake 工程创建、构建和安装之后,这一节我们了解一下库,包括静态库(.a)和动态库(.so)。一般库工程大概如下:
1 | . |
与上一个例子的区别只体现在放置工程源代码的目录变成 lib,而不是 src,其实就换个名字,换汤不换药。
我们在源代码工程 lib 目录下添加 hello.c,hello.h 和 CMakeLists.txt。
1 | // ./lib/hello.h |
以库的形式提供 HelloFunc() 函数。
1 | // ./lib/hello.c |
HelloFunc() 函数就一简单的 hello world 功能。
1 | # ./lib/CMakeLists.txt |
ADD_LIBRARY 指令用于生成目标库,动态库或者静态库;SET_TARGET_PROPERTIES 可以用于修改目标文件的属性,上面例子依次为动态库的版本信息、静态库的输出名。具体的使用方法如下:
1 | # libname 不需要写全 libhello.so,只需要填写 hello 即可,CMake 系统会自动为你生成 libhello.X |
在上面的例子中,hello 作为一个 target 是不能重名,所以,只能为动态库和静态库创建不同的 target。另一方面,又想保证动态库静态库的名字均为 libhello.X,所以可以通过修改静态库文件输出名属性的方法来满足需求。
此外,按照规则,动态库是应该包含一个版本号的。按照上述例子进行构建,生成的动态库如下。可以看出,除了生成的 libhello.so.1.2,其他两个是到该动态库的链接,方便调用者灵活调用。
INSTALL 指令和之前类似,这里要注意的是,静态库要使用 ARCHIVE 关键字,另外,安装的时候需要将库的 .h 头文件一并安装。
1 | INSTALL(TARGETS hello hello_static |
顶层目录下的 CMakeLists.txt 与上一个例子基本一致。
1 | # ./CMakeLists.txt |
在这个例子中,我们可以构建静态库和动态库,并完成该库的安装。
接下来这个例子,便是如何使用上面创建的静态库和动态库,其工程目录与第二个例子一致,主要区别在 src 源代码目录的内容上。
1 | // ./src/main.c |
调用通过库提供的 HelloFunc() 函数。
1 | # ./src/CMakeLists.txt |
INCLUDE_DIRECTORIES,为整个工程添加 include 路径,主要是 .h 头文件路径;LINK_DIRECTORIES 添加库链接路径;TARGET_LINK_LIBRARIES 确定目标文件链接库信息。具体的语法如下:
1 | # 用来向工程添加多个特定的头文件搜索路径,路径之间用空格分割,如果路径中包含了空格,可以使用双引号将它括起来 |
在上面的例子中,假如没有使用 INCLUDE_DIRECTORIES 指令,会找不到 hello.h 头文件:
假如没有使用 LINK_DIRECTORIES 指令,则会找不到 HelloFunc() 函数的具体实现:
TARGET_LINK_LIBRARIES 可以链接动态库,如下。ldd 表明生成的目标可执行文件依赖于我们上一个例子安装的动态库。
TARGET_LINK_LIBRARIES 也可以链接静态库,如下。HelloFunc() 函数以静态库的形式提供给目标可执行文件,不需要像动态库一样动态加载,已经与源文件一同生成目标可执行文件。
CMake,变量
引用:
前面我们已经提到了,使用 ${variable_name} 进行变量的引用。在 IF 等语句中,是直接使用变量名而不通过 ${variable_name} 取值。
自定义变量方式:
主要有隐式定义和显式定义两种。
使用 SET(variable_name value) 指令,显式定义变量。
PROJECT 指令,会隐式的定义
常用变量:
CMAKE_BINARY_DIR/PROJECT_BINARY_DIR /
这三个变量指代的内容是一致的,如果 in-source build,指的是工程顶层目录;如果是 out-of-source build,指的是工程编译发生的目录。
CMAKE_SOURCE_DIR/PROJECT_SOURCE_DIR /
这三个变量指代的内容是一致的,不论采用何种编译方式,都是工程顶层目录。
CMAKE_CURRENT_SOURCE_DIR
指的是当前处理的 CMakeLists.txt 所在的路径,比如上面例子中的 src 源代码子目录。
CMAKE_CURRRENT_BINARY_DIR
如果是 in-source 编译,它跟 CMAKE_CURRENT_SOURCE_DIR 一致;
如果是 out-of-source 编译,他指的是 target 编译目录。
使用我们上面提到的 ADD_SUBDIRECTORY(src bin) 可以更改这个变量的值。
使用 SET(EXECUTABLE_OUTPUT_PATH <新路径>) 并不会对这个变量造成影响,它仅仅修改了最终目标文件存放的路径。
EXECUTABLE_OUTPUT_PATH 和 LIBRARY_OUTPUT_PATH
分别用来重新定义最终结果的存放目录。
调用环境变量:
使用 $ENV{variable_name} 指令就可以调用系统的环境变量了。
系统信息变量:
UNIX
在所有的类 UNIX 平台为 true,包括 OS X 和 cygwin。
WIN32
在所有的 win32 平台为 true,包括 cygwin。
1 | # 判断平台差异,控制在不同的平台进行不同的控制 |
CMake,指令
ADD_EXECUTABLE、ADD_LIBRARY、ADD_SUBDIRECTORY
ADD_TEST 与 ENABLE_TESTING 指令
INSTALL 指令
IF 指令
ELSE ELSEIF ENDIF
NOT AND OR
1 | # 一般用法,有点别扭 |
WHILE 指令的语法是:
1 | WHILE(condition) |
FOREACH 指令
============
参考资料: 《CMake Practice》