CMake初步(1)

时间:2021-09-13 21:14:05

转自:《你所不知的OSG》第一章:CMake初步(1)http://bbs.osgchina.org/forum.php?mod=viewthread&tid=1189&fromuid=3434

本章的主要目的并非介绍OpenSceneGraph的常用类和功能,而是介绍它的一个重要伙伴,抑或说,是一个被愈来愈多的开源软件所青睐的强劲的辅助开发工具——CMake。

CMake的定义是:一个跨平台、开源、可扩展的软件编译生成系统,如果您熟悉与之同类的QMake(开源开发库Qt的自动工程生成工具)或者Automake(Unix系统的常见工程生成工具)的话,想必更有助于对它的深入了解。用一句话来描述的话,CMake的工作就是:辅助生成不同平台上的Makefile脚本,从而建立整个软件工程的编译生成规则,以及它内部以及它与其它软件工具之间的依赖关系。

不要认为Makefile只是Unix/Linux程序员的专属用品;Windows的程序开发同样离不开Makefile的概念。事实上,那些层出不穷的强大得令人眼花缭乱的IDE环境(譬如,家喻户晓的Visual Studio)为开发者们提供了太多的便利条件,以至于大家都渐渐学得好吃懒做起来——按个按钮,找个菜单项,或者极不情愿地在命令窗口中输入一串“start”字符……一切就可尽在掌握?抱歉,这世界上不会有那么多的便宜事。更多的时候,您不得不用着最简陋的文本编辑器,一行一行敲击,一行一行地耕耘。

以上就是我们即将面临的学习内容——已经悠悠地打着哈欠了吗?对于那些没有Visual Studio,没有窗口和进度条,甚至没有鼠标可用的情景,一点都不愿理会吗?呃,那么敬请期待笔者的下一篇文字。(^_^)

不然的话,欢迎进入*教程《你所不知的OSG》的第一章。工欲善其事,必先利其器。现在就来尝试了解一下——也许你还有所不知的辅助编译工具,CMake。

1.1 CMake概述

善用兵者,役不再籍,粮不三载。

CMake也许就可以喻为这样一位英武的大将。虽然看起来那么麻烦,那么深不可测,但是真正摸透它的脾气禀性之后,却能够得心应手,进而统领千军万马,事半而功倍。

CMake的核心是脚本配置文件,也就是CMakeLists.txt这个简单的脚本文件,浏览一下OpenSceneGraph的源代码目录就可以发现,从根目录开始,几乎每一个包含了子工程源代码的目录(src,examples,applications等)都会同时附带一个CMakeLists.txt。在Windows下编译OSG时,只要轻松地将根目录的CMakeLists.txt拖动到CMake-GUI的窗口中,再进行相应的选项设置,就能够生成OSG的VisualStudio工程文件(或者,以笔者的喜好,生成nmake可用的Makefile文件),进而编译得到OSG动态链接库。

Linux用户所需的工作如出一辙,在根目录下执行:

  1. # cmake . -DCMAKE_BUILD_TYPE=Release

复制代码

然后直接调用生成的Makefile脚本:

  1. # make; make install

复制代码

如此而已。

但是,试图直接使用子目录的CMakeLists.txt却是无效的,原因很简单,系统找不到许多在根目录的CMakeLists文件中配置的参数和宏,因而会产生错误提示,无法继续执行。
CMake可以针对不同的操作系统和IDE环境生成不同的脚本或工程文件,例如,VisualStudio解决方案,Mac OSX的XCode文件,Unix/Linux系统的Makefile文件等等。

说了这么多,不知您从中摘出了多少对自己有用的信息呢?也许您还在被那些恼人的问题折磨着吧——我怎么生成不了FreeType插件呢?OSG怎么找不到我的第三方库呢?那么多的配置选项都是什么意思呢?本文无力解答这么多的问题,也许其中一些会在后继的文字中得到解答,也许其中一些笔者和其他研究者们也未曾探究过,也许其中一些只有您自己钻研得出答案……也许,也许您也曾一闪念想过,如果我的工程也使用CMake来配置,然后一个命令就整整齐齐地输出链接库和可执行文件来,让其他挠着头皮的同事们乍舌不已——那该有多么潇洒呢?

那么,这才是本文的编写目的所在:就是设法帮助您,初步初步学会使用CMake编写自己的工程配置脚本,初步学会理解和阅读他人的CMake配置脚本代码,并因而能够独力阅读和理解OpenSceneGraph那繁多的配置选项,并在不算浩瀚的CMake脚本代码中(总比OSG的源代码要简单许多)找寻它们的芳踪。

1.2 CMake脚本基本知识

不知读者朋友们学习一门新的程序语言时,第一想要了解的是它的哪一方面内容?语法?关键字?应用范围?函数接口?这些当然都很重要,不过本文却要首先详解CMake脚本语言的组织结构,这将有助于您对这个陌生工具的全面理解,并且在面对版本更替和新的功能实现时,不会一头雾水,而是有的放矢,忙而不乱。

CMake包含了以下几个基本概念:

  • CMakeLists.txt

之前已经简单地介绍过了,这是CMake脚本代码和配置参数的载体,源代码目录中没有它的话,一个工程就不可能使用CMake的配置程序来完成自动Makefile脚本的生成工作。

  • 源码树和二进制树

源代码树(Source Tree)和二进制树(Binary Tree)的含义很好理解:前者表达了一个工程的所有头文件(.h,.hxx,.hh,无扩展名,等等),源文件(.c,.cc,.cpp,.cxx,等等),CMake脚本(CMakeLists.txt),以及它们的目录树结构;后者则包括平台相关的解决方案或Makefile脚本,目标文件(.obj),编译后的动态/静态库和可执行文件,以及其它编译过程中生成的文件等。

CMake允许“源码树内生成”(in-source build)和“源码树外生成”(out-of-source build)这两种工作方式。前者将会在源代码的同一目录下生成对应的Makefile脚本,目标文件以及结果;后者则是在不同的目录下执行编译生成的工作,源代码树则保持原样,十分有利于代码的版本更新,搜索管理,以及打包再发布。

对于Windows用户,可以在CMake-GUI的“Where to build binaries”栏中输入新的工作路径以实现out-of-source的模式;Linux用户则可以简单地在外部目录执行cmake指令,例如:

  1. # cmake /home/myproject –DCMAKE_BUILD_TYPE=Release

复制代码

这里的/home/myproject即是工程的根目录,其中必须包含有CMakeLists.txt文件。

对于发展和功能增补十分迅速的OSG而言,“源码树外生成”当然是首选的编译方案。这样的话,当我们不满于动辄上G的临时文件容量之时,只需要随手删除其所在目录即可,不会影响到OSG源代码分毫。

  • CMakeModules模块

一个工程需要依赖于另一个工程的头文件和库文件(.lib),无论对商业还是开源软件的开发流程来说,这都是不可或缺的一部分:GUI开发库Qt的部分功能依赖于libJPEG、libPNG;商业GIS引擎Skyline依赖于GDAL;就连微软的一些大型游戏都会依赖于开源工程OpenAL。

那么,如何告诉我们的工程,这些头文件和LIB文件的位置呢?熟悉Visual Studio环境的朋友当然知道,在工程属性的“C/C++”和“链接器”选项卡中,可以分别设置它们的路径;而Linux编程时,则需要在脚本中手动添加-I以及-L、-l参数,保证#include宏不致无所适从,以及编译器不会产生该死的LNK2001,LNK2019错误。

而对于使用CMake生成自动脚本的开发者来说,寻找头文件和库文件的工作,就交给CMakeModules中的各个模块来完成。

在OSG的根目录下有一个不太引人注意的子目录文件夹,就是这个CMakeModules。其中的文件内容十分丰富,名称则一目了然:FindFreeType.cmake,FindGDAL.cmake,诸如此类。倘若用户机的设置得当,这些扩展名为.cmake的搜索脚本可以自动获取依赖库的路径信息(但是并不会主动将它们追加到工程属性中);不然的话,我们在配置OSG时屡屡需要设置的FREETYPE_INCLUDE_DIR,GDAL_LIBRARY等选项,也是出自这些脚本的手笔。

具体的搜索脚本阅读和编写方法稍后再说。感兴趣的朋友不妨现在就用文本编辑工具打开一瞧,没准它们也并不像您想象的那样复杂也说不定。

  • CMake基本宏

细心的您想必已经发现了,前文我们在描述Linux下的CMake命令行时,有一个未作介绍的宏参数:-DCMAKE_BUILD_TYPE=Release。它可以被简单地分割为三个部分,-D是命令前缀词,CMAKE_BUILD_TYPE是宏命令的关键字,而Release则是对其赋值。这个内置宏标志的含义应当说不言而喻,它设置了工程即将采用的编译类型,可使用的值通常包括Debug、Release、RelWithDebInfo和MinSizeRel四种。和我们在Visual Studio等工具中所作的设置相似,这将改变工程的调试等级和编译生成的信息等诸多内容。

除了CMAKE_BUILD_TYPE之外,CMake中还包含一些基本的内置宏指令,典型的例如:

CMAKE_MODULE_PATH,设置搜索CMakeModules模块(.cmake)的额外路径。

CMAKE_INCLUDE_PATH,设置自动查找依赖工程头文件的额外路径,默认为脚本中指定的搜索路径。

CMAKE_LIBRARY_PATH,设置自动查找依赖工程库文件的额外路径,默认为脚本中指定的搜索路径。

CMAKE_INSTALL_PREFIX,设置安装时的路径。这是一个重要的配置参数,当链接库和可执行文件的生成工作完毕时,往往需要将这些.lib,.dll,.exe和头文件拷贝到一个独立的文件夹下,以备调用和再次复制。在Visual Studio环境下我们通过INSTALL工程来完成这一安装工作;而Unix/Linux下则是熟悉的make install。默认的安装目录为/usr/local/或者C:/Program Files/。

  • CMake缓存信息

当我们使用CMake生成了工程的解决方案或者Makefile脚本之后,再进入二进制树目录,也就是编译过程文件存储的位置(in-source模式为源代码同一目录,out-of-source模式为用户自行指定的文件夹),可以看到新生成的CMakeCache.txt文件,即缓存信息文件。

CMakeCache.txt中存储了所有自动搜索或者手动配置的路径和脚本参数。当我们更新了工程的源代码,并准备重新进行编译时;使用这种缓存信息文件可以有效地加速CMake配置的过程,方法是直接将这个文件拖动到CMake-GUI窗口中,或者在命令行方式下执行:

  1. # cmake –C CMakeCache.txt

复制代码

此时系统将自动读入上一次配置的所有信息,使用CMake-GUI的用户可以在对话框中再次进行参数的修改,并生成新的解决方案或者Makefile文件,以供下一步的工程编译生成工作。

1.3 你好,世界

CMake是一种可扩展的脚本语言,要学会阅读和编写CMake脚本代码,就必须要理解它的基本词法和语法,以及理解它的接口扩充法则。而这一切的学习又都是建立在实践的基础上的,否则再好的教程课本和参考文档都只是废纸一张。

那么,是先罗列枯燥的关键字好呢?还是先打开任意的文本编辑器,小试牛刀一把好呢?笔者窃以为后者是通往CMake入门者殿堂的更快途径。

那么就让我们在不了解任何CMake脚本规范条目的前提下,开始CMake工程涉及的旅程吧。是不是有点疯狂呢?没关系,我们就用开发者最喜欢的“你好,世界”(Hello World)作为第一个实践的项目。编码过程中所涉及的各种词法和语法规则,我们仅仅稍作解释,只求给读者留下CMake脚本开发的初步印象,以便在阅读下一小节时能够更加得心应手。

现在我们假设有这样一个工程,它包括一个名为HelloLib的动态链接库,以及一个依赖于这个库的Test可执行程序。其文件夹结构为:

CMake初步(1)

HelloLib子工程定义并实现了一个名为Hello的类,这个类只有一个公有方法sayHello(),作用是在屏幕上打印“Hello CMake”这一行简单的字符。为此,这个目录下将包含头文件Hello以及源文件Hello.cpp,并期望能够生成HelloLib.dll的动态链接库,以便所有依赖于HelloLib的程序可以因此打印出友好的欢迎语句。(尽管这看起来很幼稚 ^_^)

Test子工程只有一个源文件test.cpp,它的工作仅仅是在main函数中调用sayHello()方法并即刻结束自己的使命。Test因而要依赖于HelloLib库存在,并期望生成一个名为Test.exe的可执行文件,在控制台窗口中执行自己的简单任务。

这三个文件的源代码如下:

  1. /* Hello */
  2. #ifndef H_HELLOWORLD
  3. #define H_HELLOWORLD
  4. #if defined(_MSC_VER) || defined(__CYGWIN__) || defined(__MINGW32__)
  5. #    ifdef HELLOWORLD_LIBRARY
  6. #        define HELLOWORLD_EXPORT __declspec(dllexport)
  7. #    else
  8. #        define HELLOWORLD_EXPORT __declspec(dllimport)
  9. #    endif
  10. #else
  11. #    define HELLOWORLD_EXPORT
  12. #endif
  13. class HELLOWORLD_EXPORT Hello
  14. {
  15. public:
  16. void sayHello();
  17. };
  18. #endif

复制代码

  1. /* Hello.cpp */
  2. #include <iostream>
  3. #include "Hello"
  4. void Hello::sayHello()
  5. {
  6. std::cout << "Hello CMake!" << std::endl;
  7. }

复制代码

  1. /* test.cpp */
  2. #include <Hello>
  3. int main( int argc, char** argv )
  4. {
  5. Hello obj;
  6. obj.sayHello();
  7. return 0;
  8. }

复制代码

现在我们给每一个文件夹中都追加一个CMakeLists.txt文件(因此共有3个),从而形成完整的CMake自动编译脚本树。这其中,工程根目录下的CMakeLists.txt决定了工程的基本属性,并使用ADD_SUBDIRECTORY,我们暂且称之为“命令”(Command),指向每一个工程子目录;而子目录下的CMakeLists.txt则决定了当前工程的属性,依赖关系,源代码文件以及编译规则等等。

根目录下的CMake脚本实现代码如下:

  1. PROJECT( HelloWorld )
  2. CMAKE_MINIMUM_REQUIRED( VERSION 2.4.7 )
  3. ADD_SUBDIRECTORY( HelloLib )
  4. ADD_SUBDIRECTORY( Test )

复制代码

不要急于看下面的内容,先看看这段CMake脚本代码——也许您会发现它们其实很容易读懂对不对?首先指定整个工程的名称为HelloWorld,然后指定“所需的最低CMake版本”为2.4.7,并且添加两个子目录HelloLib和Test——这仅仅是我们根据英文关键字翻译过来的内容,但也恰恰是这段脚本想要表达的意思。这里面的关键是ADD_SUBDIRECTORY命令,它指示系统到下一级子目录去搜索CMakeLists.txt脚本的位置,并执行具体子工程的生成任务。

子工程HelloLib的任务是:生成动态链接库HelloLib.dll(或者libHelloLib.so,Unix/Linux系统中),定义一个Hello类并指示动态链接库将其输出,以便依赖于HelloLib库的程序可以声明其实例并调用它的功能。

它的脚本实现代码如下:

  1. ADD_DEFINITIONS( -DHELLOWORLD_LIBRARY )
  2. ADD_LIBRARY( HelloLib SHARED
  3. Hello.cpp
  4. )

复制代码

这里出现了另一个重要的命令:ADD_DEFINITIONS,用于定义程序中所需的预编译宏,其固有参数格式通常是-D加上宏的名称。例如此处定义了一个HELLOWORLD_LIBRARY宏,进而在程序中设置了Hello类的输出方式。(Win32下往往使用dllexport来指定函数和类的输出,其它系统则通常不必特殊指示)当然使用#define来进行定义也是相同的作用,但是能够在编译脚本中有选择地完成此类工作的话,自然会给工程带来更大的灵活性和平台兼容能力。

第二个重要的命令是ADD_LIBRARY,很明显,它的任务是指示CMake系统添加一个新的链接库(Library)子工程,其参数格式为:

  1. ( 库名称 库类型 源文件 )

复制代码

库名称为HelloLib,这也是最终生成的链接库的名称;类型为SHARED,即动态链接库(.dll或.so),而STATIC自然就表示生成静态链接库了(.lib或.a);库所需的源代码文件只有一个,即Hello.cpp,其中所包含的头文件Hello因为和.cpp文件处于同一目录下,因此不必再特地指定。

不要在意这里ADD_LIBRARY书写的缩进格式,甚至不必在意其大小写,它只是一种排版和增加易读性的方式而已,并无强制要求。

就这么简单,一个新的链接库工程就建立完成了。如果使用CMake自动生成Visual Studio解决方案的话,应该可以看到这个子工程的身影了。什么?您的做法是选择Visual Studio菜单的“文件->新建->工程”,并且认为这样更加简单?目前也许如此,不过为何不晚些再下定论呢?

那么,按照我们之前的约定,现在要建立一个名为Test的可执行工程(.exe),并通过它来调用Hello::sayHello()方法,打印字符串“Hello CMake!”。

使用Visual Studio应该如何来操作呢?在同一个解决方案中再次选择“文件->新建->工程”;选择建立一个Win32控制台工程,将test.cpp拖动到源文件目录;下一步,在“工程属性->C/C++”中指定“附加头文件目录”为Hello所在的目录;在“工程属性->链接器”中指定“附加库文件”为HelloLib.dll;然后指定HelloLib和Test两者的依赖关系,编译生成,一切完毕——嗯,有些麻烦对不对?也许此时您会稍作期盼,CMake的做法能否像刚刚建立链接库那样简单呢?您说对了,就是那样简单:

  1. INCLUDE_DIRECTORIES( ${PROJECT_SOURCE_DIR}/HelloLib )
  2. ADD_EXECUTABLE( Test
  3. test.cpp
  4. )
  5. TARGET_LINK_LIBRARIES( Test HelloLib )

复制代码

这里涉及到一个“变量”的概念,即${PROJECT_SOURCE_DIR}。对于精通C/C++编程的您来说,这类情形其实再熟悉不过,例如:

  1. std::string str1 = “abc”;
  2. std::string str2 = str1 + “def”;
  3. std::string str3 = “str1 + def”;

复制代码

您一定知道,这里str2的赋值为“abcdef”,而str3则是令人匪夷所思的“str1+def”。变量str1的职责和作用范围也因此不言而喻。

CMake中的变量也是如此:我们可以使用SET命令来设置和传递各种用户变量,也可以直接使用系统内置的全局变量,例如此处的PROJECT_SOURCE_DIR。注意所有对于变量值的使用都需要遵循“${……}”的格式,否则它会被识别为普通的字符串,就像str3中存储的结果那样;但是对于CMake而言,就算是被引号包含,也同样可以使用“${……}”来指定使用变量中的内容,例如:

  1. SET( MY_SYSTEM “CMake building system” )
  2. MESSAGE( “${MY_SYSTEM} is good for developers.” )

复制代码

将显示“CMake building system is good for developers.”这一句完整的对白,而非毫无意义的MY_SYSTEM符号。当然,聪明的您也不需要笔者过多解释此处MESSAGE命令的意思——它想必是用来显示调试或警告信息的,通过控制台窗口或者对话框。

注意,CMake的变量是可以“自省”的。如果我们现在有一个名为MY_NAME的变量赋值为“WANG”,那么${MY_NAME}_LIB变量也就等同于WANG_LIB变量,在脚本中对于${${MY_NAME}_LIB}的使用也就等同于对${WANG_LIB}的使用。

还有一点需要注意,CMake中的变量类型只有•字符串•这一种!但是它却可以被不同的脚本命令灵活地识别为字符串、布尔型、整型或浮点型,这样也省却了类型转换的种种麻烦。

这样繁琐的讲解是否有助于您的学习理解呢,抑或是啰嗦得使您觉得厌烦了?那么我们回到脚本代码中来。明白了PROJECT_SOURCE_DIR的含义,您自然也能猜到这个变量中的内容是什么,也就是整个工程的根目录了。它由系统自动检测和赋值,不需要我们操心;而这个变量对于各个子工程之间的依赖关系设置,以及头文件和源文件路径设置显然有着至关重要的作用!这一点不言自明。

在Test子工程的CMake脚本中,我们首先使用INCLUDE_DIRECTORIES命令设置附加头文件目录的位置;然后用ADD_EXECUTABLE指定要编译的可执行程序名称和相关源文件;最后使用TARGET_LINK_LIBRARIES设置Test所依赖的链接库文件,即刚刚设置生成的HelloLib库。

一切就是如此。那么,启动您的CMake-GUI窗口或命令行,生成您钟爱的工程或者Makefile,并且编译运行。结果精致而简单,过程也不复杂,收获不多也不少——这样的感觉,不是很惬意吗?
CMake初步(1)