欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

CMake进阶之初识CMake

程序员文章站 2022-04-05 08:50:05
...

    平时开发中我们已经习惯了让IDE为我们做好一切,大部分情况下基本上不需要手动去编写项目的make文件,但是在规模较大的项目中,make其实非常重要,甚至可以说会不会make决定了你是否真的了解项目的整体架构并驾驭它。因为自己在Android开发中发现项目中的NDK部分已经抛弃了传统的Android.mk,与时俱进用上了CMake,因此打算静下心来好好学习学习,本文开始对学习CMake的过程做个笔记,以加深印象。参考书籍是《mastering cmake》,这应该算得上是关于cmake的一部经典之作,感兴趣的读者可以下载电子书或者购买一本学习。

1 为什么要用CMake

    大家可能发现现在CMake用的越来越多,就以Android为例,以前NDK开发时都是用的安卓特有的Android.mk,但是现在基本上都用CMake了,虽然Android.mk依然支持,但是笔者所参与的项目中,它已经被无情的抛弃了。为什么用CMake?简单一句话概括就是:

    跨平台:一份make可以支持多个平台。

    想象一下牛逼的你写了一个牛逼的库,然后你想让多个平台的开发者都能享用你的库。假如写这个库的时候还没有CMake这个东西,那么你要怎么办呢?你必须写一份unix系统上的make file以便这个库能够在类unix系统上构建;然后你得搞份Android.mk以便它同样能在Android上构建;你还得考虑Windows上用visual studio的开发者......,如此众多的平台,想想都头疼。

    CMake就是帮我们解决这个问题的,它让make的编写对特定的平台透明:开发者只需要按照CMake的语法写make,不需要考虑具体平台,最终由CMake为我们生成原生的构建工具(比如Windows上的visual studio,Mac上的XCode,unix/linux的make)所需要的构建文件。使用CMake可以让我们享受到很多的福利,我们列举出一些来感受下:

    (1) 自动搜索你的软件所依赖的库、头文件,CMake在搜索的时候会将环境变量和注册表(Windows平台)也包含在内;

    (2) 项目的构建目录和源码目录分离:也就是说可以在项目源码目录之外单独建立一个构建目录,用于存放构建过程中生成的文件。比如下面这样的目录结构:

    + src  //项目的源码

        |

        -------- main.cpp

        |

    + bin  //项目的构建目录

        |

        -------- test.so

    构建目录和源码目录分离,使得我们可以随时删除构建文件而不用担心会误删掉项目的源码文件。

    (3) 在配置阶段选择可选的组件:例如你的项目需要用到某个功能,有两个库都可以完成此功能,其中一个库体积大但是效率很高,另一个库体积小但是效率较低。通过CMake你可以方便的根据你项目的情况决定选择哪个库链接,例如Window平台,你可以选择体积大,效率高的库,而Android平台考虑到移动终端容量限制,你可以选择使用体积小,效率低一点的库。

    (4) 很容易在共享库和静态库的构建上进行切换:你可以方便的指定是要构建共享库还是静态库,CMake在背后帮我们处理了构建共享库所需要的平台相关的链接器选项。

    (5) CMake能自动生成项目文件的依赖关系,同时绝大多数平台上支持并行构建。

    如果你在开发一个跨平台项目,比如说一个跨平台的视频播放器,CMake还会带来一些额外福利:

    (1) CMake可以帮我们检测机器的字节序以及一些特定于硬件的信息,这一点很重要,在跨平台项目中,忘记字节序是一些很难追查的bug的根源之一;

    (2) 如前文所说:一份cmake构建脚本在多个平台上使用。

2 CMake的历史

    CMake如此牛逼,它是怎么来滴呢?CMake最初是ITK项目的一部分,ITK项目始于1999年,由美国麦迪逊国家实验室赞助。ITK项目规模比较庞大,并且需要在多个平台上运行,同时还依赖于其他一些软件库。为了满足软件的构建需求,需要一个功能足够强大同时又简单易用的构建工具,于是ITK项目的开发者设计出了CMake来满足需求。当CMake诞生以后,因为它的灵活易用和强大的功能,越来越多的项目中都用上了它,这其中最著名的案例就是KDE,一个庞大的开源项目,KDE采用了cmake来进行构建,这也证明了cmake确实是一个能够解决大型项目构建问题的解决方案。

    CMake最近一次的更新中,加入了CTest和CPack,CMake能够支持软件测试,而CPack则支持跨平台的软件发布,CPack利用已有的比较受大众青睐的工具包比如RPM,Cygwin,PackageMaker等,创建各种原生系统上的软件安装包。

    当然CMake还加入了其他一些有用特性,例如支持XCdoe和Visual Studio 10。CMake现在还支持嵌入式设备和其他操作系统的交叉编译。总之CMake作为一个开源项目,自身在不断的进化,功能也会越来越强大。

3 初识CMake

    本节我们通过一个没有什么实际意义的示例项目,来演示一下CMake的用法,让大家对CMake建立一个宏观印象,一些细节可以不用太关注,后续文章还会有更详细的说明。

3.1 示例项目

    示例项目是这样的:

    (1) 我们自己写了一个数学函数库mymath,提供一些数学运算。

    (2) 项目最终生成一个可执行文件Test,它连接我们的mymath数学库完成相关运算。

    (3) 我们的项目需要使用boost库的相关特性,因此可执行文件Test还需要链接boost库。

    (4) 需要使用C++11提供的一些特性;

    (5) 在MAC系统上,我们使用clang++来编译,在linux系统上我们使用g++编译;

    再次说明不要关注这个示例的代码,我们只是用它来演示CMake的相关特性。示例项目的目录结构如下:

   CMake进阶之初识CMake

    从上面的目录结构中注意这样一个事实:根目录Test和数学库目录math下各有一个CMakeLists.txt,这是因为math目录下将编译出一个静态库目标文件:my_math静态库,而利用根目录下的文件将编出可执行目标:Test。

    我们将构建目录和源码目录分离,Test目录为项目的源代码目录,bin目录为构建目录,存放构建过程中生成的文件。

3.2 数学库

    我们的数学库非常简单,提供一个计算任意数平方的接口和一个统计数组中小于某个值的数字的个数的接口,头文件如下:

//计算任意数的平方
template <typename T>
auto square(const T& t) -> decltype(t * t) {
	return t * t;
}

//统计数组中小于指定数的个数
extern int count(int *arr, int len, int val); 

    注意到计算任意数平方的函数,我们使用了C++11的特性。

    math.cpp的实现非常简单,这里就不在贴出来了。

    然后我们需要在math子目录下编译出一个静态库目标,在math子目录下添加一个名为CMakeFileLists.txt的文件,cmake将解析这个文件,执行其中的命令。对于示例程序而言很简单,只需要告诉cmake根据指定的c++源文件生成一个静态库而已,因此math/CMakeFileLists.txt就一句话:

#生成一个静态库目标
add_library (${MATH_LIB} STATIC math.cpp)

    CMake的命令具有如下这样的格式:

    command ( arg1 arg2 .... )

    其中command为cmake支持的命令,括号中是提供给命令的参数,参数之间以空格分开。

    上面这条add_library命令,告诉cmake生成一个库目标,第一个参数表示生成的库目标的名字,这里取上层目录传入的变量MATH_LIB的值作为目标名,第二个参数STATIC表示生成静态库,随后的参数math.cpp告诉cmake编译静态库需要的源代码文件。

    关于目标(target)的概念,我们放在下一篇文章在解释,现在只需要知道target通常是一个可执行文件或者是库文件就可以了。

    好了,到目前为止,我们已经知道了cmake中的一些概念:命令(command),目标(target)和变量,我们还看到了CMAKE中取变量值的方法:${VAR},而且我们也知道了用add_library命令给项目生成一个库目标(静态/动态)的方法。

3.3 可执行文件

    我们已经写好了数学库的CMakeFileLists.txt,现在我们需要写一个程序来引用数学库了,直接在main.cpp中做就好了:

#include <iostream>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <thread>
#include <functional>
#include <math2.h>

using namespace std;

static void square_int(int i) {
	cout << "square(" << i << ")=" << square(i) << endl;
}

static double square_double(double d) {
	cout << "square(" << d << ")=" << square(d) << endl;
}

int main(int argc, char **argv) {

	int array[10] = {5, 20, 30, 6, 0, 40, 3, 100, 9, 88};
	cout << "number less than 10:" << count(array, 10, 10) << endl;

	boost::thread_group c;
	c.create_thread(boost::bind(&square_int, 4));
	c.create_thread(boost::bind(&square_double, 5.6));
	c.create_thread(boost::bind(&square_double, 10004.135));
	c.join_all();

	return 0;
}

    再次申明示例代码只是演示cmake用,不要在意它的实现方式。在main中,我们使用了boost库的thread_group,向线程组中添加了3个线程,线程引用了数学库的square方法来计算平方,另外还调用数学库的count方法统计了一个数组中小于10的数字的个数。

    OK,目前为止这个示例程序已经用到了boost库和c++11的特性,同时还用到了我们自己的数学库的功能,我们看看如何编写CMakeFileLists.txt,让cmake将这一切整合起来,为我们生成可执行文件,我们来看看最终的CMakeFileLists.txt:

#一般都以这一行开始
cmake_minimum_required (VERSION 2.6)

#项目名
project (TEST)

#选择编译器,LINUX上选择g++,MAC OS上选择clang++
if (APPLE)
	set (CMAKE_CXX_COMPILER clang++)
elseif (UNIX)
	set (CMAKE_CXX_COMPILER g++)
endif()

#设置编译器选项支持c++11
set (CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -std=c++11)

#查找boost库
find_library (BOOST_SYSTEM
	NAMES boost_system
	PATHS /usr/lib /usr/local/lib
	)

find_library (BOOST_THREAD
	NAMES boost_thread
	PATHS /usr/lib /usr/local/lib
	)


#设置数学库的目标名,该变量在子目录也是可见的
set (MATH_LIB "my_math")

#添加头文件搜索路径
include_directories (./math)

#添加子目录,这样math目录才会被编译
add_subdirectory (math)

#添加可执行目标文件Test
add_executable (Test main.cpp)

#LIBS变量存储所有需要链接的库
set (LIBS ${MATH_LIB})

if (BOOST_SYSTEM) 
	set (LIBS ${LIBS} ${BOOST_SYSTEM})
endif()

if (BOOST_THREAD)
	set (LIBS ${LIBS} ${BOOST_THREAD})
endif()

#为可执行文件链接数学库
target_link_libraries (Test "${LIBS}")

    首先以cmake_minimum_required命令告诉cmake编译这个项目需要的最小的cmake版本,一般都以它为开始。

    然后命令project (TEST)告诉cmake工程的名字。

    接下来我们根据当前所在的系统来指定编译器,在苹果上我们用clang++来编译项目,在UNIX系统上用g++来编译,这可以通过下面的代码实现:

    if (APPLE)
        set (CMAKE_CXX_COMPILER clang++)
    elseif (UNIX)
        set (CMAKE_CXX_COMPILER g++)
    endif()

    APPLE和UNIX是CMake内置的值,可以直接用来判断用户当前的系统。CMAKE_CXX_COMPILER是CMake内置的变量,用来告诉cmake编译器的位置,通过set命令可以设定变量的值。

    除了上面这种通过修改CMAKE_CXX_COMPILER变量的方式指定编译器的方法外,还有两种方法也可以达成这个目的:一种是在系统环境变量中增加名为CC或者CXX环境变量(分别对应C和C++编译器),另外一种方法是在命令行执行cmake命令式,通过-D选项来指定:cmake -DCMAKE_CXX_COMPILER=clang++。推荐的方式是在环境变量中指定编译器。

   继续,因为我们的示例项目中用到了c++11的特性,因此我们还要指定编译器选项以开启c++11,与设定编译器一样,可以通过set命令设定CMake的内置变量CMAKE_CXX_FLAGS来做到:

    set ( CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -std=c++11 )

    set命令可以给变量设置一组值,例如set (Foo a b c),则变量Foo可以理解为是一个字符串数组:Foo = {a, b, c}。假设cmake执行了下面两条指令:

    set (Foo a b c)

    set (Foo ${Foo} d)

    则最终变量Foo = {a, b, c, d}

    同样的,除了设置CMake内置变量的方式外,编译器选项也可以在环境变量中设置:CFLAGS和CXXFLAGS分别对应C和C++编译器的编译选项。

    如果在环境变量中设置编译器和编译器选项,上面的这一部分代码就可以从CMakeFileLists.txt中干掉了。

    现在,我们已经开启了C++11的特性,接下来,我们要准备将我们自己的数学库加入进来:

    set (MATH_LIB "my_math")
    include_directories (./math)
    add_subdirectory (math)

    还记得之前math目录的CMakeFileLists.txt吗?在那里我们用add_library生成一个名字由变量MATH_LIB的值指定的静态库目标,那里用到的变量MATH_LIB就是在这里传入的。由此我们也知道了CMake中外层创建的变量,会自动传递给子目录。我们用set命令指定数学库目标的名字为my_math。

    然后我们用include_directories命令来指定头文件的搜索路径,这相当于编译器的-I选项,告诉编译器去哪里找头文件。这样我们在代码中可以直接通过#include <math2.h>的方式引入数学库头文件。

    最后用add_subdirectory命令将数学库的子目录math加入进来,这一步是必须的,只有这样,math子目录下的CMakeFileLists才会被cmake解析并构建出指定的target。

   现在,math数学库也已经准备好了,接下来就需要加入boost库的支持了,我们直接用CMake的find_library来查找需要的库,在示例程序中,需要用到两个boost库:boost_system和boost_thread:

    find_library (BOOST_SYSTEM

        NAMES boost_system

        PATHS /usr/lib /usr/local/lib

    )

    find_library (BOOST_THREAD

        NAMES boost_thread

        PATHS /usr/lib /usr/local/lib

    )

    find_library命令用来查找库,第一个参数用来存放查找的结果,第二个参数NAMES指定要查找的库的名,可以有多个,例如boost的thread就有boost_thread和boost_thread_mt两个版本,PATHS用来指定在哪里搜寻指定的库。现在我们用一个变量LIBS把程序需要链接的库都保存起来:

    set (LIBS ${MATH_LIB})

    if ( BOOST_SYSTEM)

        set (LIBS ${LIBS} ${BOOST_SYSTEM})

    endif()

    if (BOOST_THREAD)

        set (LIBS ${LIBS} ${BOOST_THREAD})

    endif()

    万事俱备,只欠东风,最后我们就要让cmake为我们生成可执行文件了:

    add_executable (Test main.cpp)

    add_executable命令生成一个可执行的目标,第一个参数为可执行目标的名称,随后的参数是构建该目标需要的源文件。

    光这样还不行,我们还得把数学库和需要的boost库链接到可执行文件上,否则就会出现undefined symbol这样的错误。通过target_link_libraries命令,我们将需要的库链接进来,示例程序需要的库已经保存在了LIBS变量中:

    target_link_libraries (Test "${LIBS}")

    target_link_libraries命令用来链接指定的库到目标,第一个参数是目标名,随后是目标需要链接的库所在路径。

3.4 运行CMake

    我们的CMakeFileLists.txt构建脚本已经编写完成了,接下来就要运行cmake,CMake有多种运行方式,可以通过cmake gui,通过图形界面的形式,也可以在命令行直接运行,这里只说明在命令行运行的方式。 

    因为示例项目采用的是构建目录和源代码目录分离的方式,对于这种结构,可以按如下步骤运行cmake:

    首先cd到构建目录bin:

    cd bin

    然后cmake,指定源代码目录:

    cmake ../Test

    如果你的CMakeFileLists脚本写的没有错误,一切正常,那么这条命令执行完以后cmake已经为我们生成了特定平台上的构建文件(例如UNIX系统上,当然就是makefile咯)以及数学库math,查看此时的bin目录,会发现生成了下面这些东西:

    CMake进阶之初识CMake

    现在cmake已经为我们生成了原生构建系统需要的项目构建文件,接下来直接make就可以了:

    make    

    如果你的代码中没有任何语法错误,那么恭喜你,系统已经为你生成了最终的可执行文件:    

CMake进阶之初识CMake

    Test就是我们最终的可执行文件了。

4 小结

    在本文介绍了CMake的历史以及为什么要用CMake,它能为我们带来什么好处。通过一个示例项目的构建展示了CMake脚本的语法和构成,让读者对CMake有一个宏观印象,最后在总结一下:

    (1) CMake是由构成项目的所有目录下的CMakeFileLists.txt构建脚本控制构建过程的;

    (2) CMake最大的好处是跨平台,一份cmake构建脚可以运用在多个平台上;

    (3) CMakeFileLists.txt主要是由命令command构成,command具有如下格式:

          command (arg1 arg2 ...)

    (4) CMake可以通过set 命令给变量设置一个或者一组值,用${VAR}的方式可以取变量的值;

    (5) 通过3种方式可以为CMake指定编译器:设定环境变量CC或CXX、cmake命令行中使用-DCMKAE_CXX_COMPILER=以及在CMakeFileLists.txt构建脚本中设定内置变量CMAKE_CXX_COMPILER的值;

    (6) 可以通过cmake gui或者直接在命令行启动cmake。

    

相关标签: cmake c 11 boost