CMake初步(2)

转自:《你所不知的OSG》第一章:CMake初步(2)

http://bbs.osgchina.org/forum.php?mod=viewthread&tid=1229&fromuid=3434

 

1.4 词法和语法

在开始本节的学习之前,我们先总结一下之前所了解到的CMake基本词法和命令。

CMake命令通常使用如下的格式:
  1. COMMAND( ARG1 ARG2 … )
复制代码
命令关键字之后使用括号来包含所有的参数;各个参数之间使用空格或者换行符分隔;而参数通常有以下几种形式:

变量,以${MY_VAIRABLE}的形式表达,其储存类型为字符串类型,但是可以根据具体命令的要求自动转换为布尔型、整型或者浮点类型。变量可以出现在字符串中,也可以实现“内省”。变量有用户自定义和系统内置两种,用户自定义变量使用SET命令设置;而系统变量由系统自动赋值,例如${PROJECT_SOURCE_DIR}。

枚举量,例如ADD_LIBRARY可以设置要生成的链接库为SHARED或者STATIC,还可以设置为MODULE(插件,可动态调用,但不作为其他工程的依赖),除此之外的赋值都是不能被识别的。

值,也就是任意的字符串内容,它可以用来指示要编译的源代码文件名,也可以表达一段文字提示信息,或者表达特定的功能。值可以使用引号进行标识,多数情况下也可以不用。

前文中我们已经了解到的命令列举如下,此外这里还简要地介绍了另一些可能在各类CMake工程中遇到的命令及其语法格式。CMake部分命令的语法歌是十分复杂,这里仅仅介绍它的某一种实现形式,建议读者阅读CMake的帮助文档以获取更多信息。

括号中为该命令的一个或多个参数项,其中使用“[…]”包含的项表示可忽略项,使用“…|…”分隔的项表示只能选择其中一项。

ADD_CUSTOM_COMMAND(
    TARGET name 
    PRE_BUILD|PRE_LINK|POST_BUILD 
    COMMAND cmd1 [COMMAND cmd2 …] ):
为目标工程name添加一个或多个新的自定义的编译规则cmd1,cmd2等,执行时机可以选择编译前,链接前或者编译后。它的作用相当于Visual Studio工程的“Custom Build Step”属性。

ADD_CUSTOM_TARGET( name COMMAND cmd1 [COMMAND cmd2 …] ):
添加一个名为name的编译目标,并指定一个或多个自定义的命令cmd1,cmd2等。注意ADD_CUSTOM_COMMAND与这个命令的区别:前者是针对一个已有的子工程进行自定义编译规则的设置;后者则是建立一个新的自定义的目标工程,例如一个专用于将已生成文件拷贝到指定文件夹的INSTALL工程;以及与之作用截然相反的UNINSTALL工程。

ADD_DEFINITIONS( -DMACRO1 –DMACRO2 … ):
添加-D预编译宏定义,可以一次添加多个。

ADD_EXECUTABLE( name [WIN32]
                    source1 source2 … ):
指定一个名为name的可执行程序工程,其源文件为source1,source2等,此外还可以追加一个枚举量WIN32,表示此程序为Win32程序,使用WinMain作为程序入口。

ADD_LIBRARY( name [STATIC|SHARED|MODULE]
                source1 source2 … ):
指定一个名为name的链接库工程,其源文件为source1,source2等,此外还可以指示该工程的生成结果为静态库(STATIC),动态库(SHARED)还是模块(MODULE)。

ADD_SUBDIRECTORY( dir ):
指示下一级CMake脚本所在位置位于dir子目录。

CMAKE_MINIMUM_REQUIRED( VERSION major[.minor[.patch]] ):
指示当前脚本所需的CMake版本,不能低于版本号major.minor.patch。

CONFIGURE_FILE( infile outfile ):
将文件infile复制到outfile的位置,同时执行其中变量的自动配置和更替,即,将infile中所有形同“${VAR}”和“@VAR@”的变量替换为对应的内容,并拷贝到outfile中,而这个新生成的outfile文件也可以在之后的脚本代码中得以使用。

FIND_LIBRARY( ${var}
                NAMES name1 [name2 …]
                PATHS path1 [path2 …]
                PATH_SUFFIXES suffix1 [suffix2 …] ):
搜索一个外部的链接库文件,并将结果的全路径保存到var变量中。要搜索的链接库文件名字可能是name1,name2等;搜索路径为path1,path2等;此外还可以指定路径的后缀词为suffix1,suffix2等。因此,系统将尝试在path1/suffix1,path1/suffix2,path2/suffix1,path2/suffix2这些目录中搜索名为name1或name2的链接库文件,并将结果(路径和文件名)保存到var中。

FIND_PACKAGE( name ):
在指定的模块目录中搜索一个名为Find<name>.cmake(例如,FindOSG.cmake)的CMake脚本模块文件,执行其中的内容,以图搜索到指定的外部依赖库头文件和库文件位置。

FIND_PATH( ${var}
            NAMES name1 [name2 …]
            PATHS path1 [path2 …]
            PATH_SUFFIXES suffix1 [suffix2 …] ):
搜索一个包含name1,name2等文件的目录,并将此路径(不包含文件名)保存到var变量中,搜索路径为path1,path2等;此外还可以指定路径的后缀词为suffix1,suffix2等。通常可以使用此命令来确认外部头文件的依赖路径。

FIND_PROGRAM( ${var}
                 NAMES name1 [name2 …]
                 PATHS path1 [path2 …]
                 PATH_SUFFIXES suffix1 [suffix2 …] ):
搜索一个外部的可执行程序,并将结果的全路径保存到var变量中。要搜索的程序名字可能是name1,name2等;搜索路径为path1,path2等;此外还可以指定路径的后缀词为suffix1,suffix2等。

INCLUDE( file ):
在当前文件中包含另一个CMake脚本文件的内容。

INCLUDE_DIRECTORIES( dir1 dir2 … ):
指定编译器搜索头文件的依赖路径,可以添加多个。

INSTALL( TARGETS proj1 proj2
         RUNTIME DESTINATION runtime_dir
         LIBRARY DESTINATION library_dir
         ARCHIVE DESTINATION archive_dir):
这只是此命令的一种语法格式,安装目标工程proj1,proj2等到指定的文件夹。其中,可执行文件安装到RUNTIME DESTINATION指定的runtime_dir目录;动态链接库安装到LIBRARY DESTINATION指定的library_dir目录;静态链接库安装到ARCHIVE DESTINATION指定的archive_dir目录。如果需要安装头文件或者数据文件,则通常使用INSTALL( FILES … DESTINATION … )的形式。

LINK_DIRECTORIES( dir1 dir2 … ):
设置外部依赖库的搜索路径。

MESSAGE( [SEND_ERROR|STATUS|FATAL_ERROR] “text” … ):
在控制台或者对话框输出一行或多行调试信息文本text,枚举量用于控制信息的类型(错误,状态显示,致命错误)。

OPTION( ${var} “text” value ):
向用户提供一个可选项,提示信息为text,初始值为value,并将最终的结果传递到var变量中。在CMake-GUI中它将以配置选项的方式出现。

PROJECT( name ):
设置整个工程的名称为name。

SET( variable value
     [CACHE FILEPATH|PATH|STRING|BOOL “text”] ):
定义一个用户自定义变量variable,取值为value。此外还可以使用CACHE关键字,允许用户在CMake-GUI中修改变量的值,修改方式包括文件对话框(FILEPATH),目录对话框(PATH),编辑框(STRING)或者复选框(BOOL),并使用text作为提示信息。

SET_TARGET_PROPERTIES( name PROPERTIES prop value ):
设置名为name的工程的属性,这里主要可选的prop属性包括PROJECT_LABEL, DEBUG_POSTFIX,OUTPUT_NAME等等,value为设置值。

TARGET_LINK_LIBRARIES( name
                           lib1 lib2 …
                           [debug|optimized] lib1 lib2 … ):
指定工程name所用的依赖库,并可以使用debug和optimized关键字分别指定DEBUG与RELEASE版本所用的一个或多个依赖库。

也许您并不一定完全明白这里所说的每一句话,这也是我们之所以把词法和语法的介绍放在“Hello World”例子之后的一个原因——没错,仅仅是这些单词的罗列未免太枯燥了。那么,为什么不马上拾起我们刚刚才完成的简单脚本工程,在上面添砖加瓦一番呢?说不定这才是您充分理解和深入学习CMake的关键呢。

是的,实践才是最好的老师。要充分理解CMake的强大之处,以及确保自己具备足够的力量去阅读OSG的CMake脚本源代码,势必还要再多做一些更为复杂的练习才行。不过在此之前,我们还是再多了解一些CMake的常用内置变量和脚本命令,以及CMake中条件语句,循环语句和宏函数的概念用法。

包括前文介绍的PROJECT_SOURCE_DIR在内,以下内置全局变量都可以在CMake脚本中以“${…}”的形式直接加以应用,以方便脚本代码的定位和功能实现:

 

  • CMAKE_BUILD_TYPE:工程的编译生成的版本类型,可选项包括Debug,Release,RelWithDebInfo和MinSizeRel。
  • CMAKE_COMMAND:也就是CMake可执行文件本身的全路径,例如/usr/local/bin/cmake或者C:\Program Files\CMake 2.6\bin\cmake.exe。
  • CMAKE_DEBUG_POSTFIX:Debug版本生成目标的后缀,通常可以设置为“d”字符,例如Debug版本的OSG核心库为osgd.dll,而Release版为osg.dll。
  • CMAKE_GENERATOR:编译器名称,例如“Unix Makefiles”,“Visual Studio 7”等。
  • CMAKE_INSTALL_PREFIX:工程安装目录,所有生成和调用所需的可执行程序,库文件,头文件都会安装到该路径下,Unix/Linux下默认为/usr/local,Windows下默认为C:\Program Files。
  • CMAKE_MODULE_PATH:设置搜索CMakeModules模块(.cmake)的额外路径。
  • PROJECT_BINARY_DIR:工程生成工作所在的目录,即前文所述的“out-of-source”的目录;对于“in-source”形式的编译工作,该变量与PROJECT_SOURCE_DIR所指向的目录相同。
  • PROJECT_NAME:工程名称,即使用PROJECT命令设置的名称。
  • PROJECT_SOURCE_DIR:工程源代码文件所在的目录。
  • CYGWIN:标识当前系统是否为Cygwin。
  • MSVC:标识当前系统是否使用Microsoft Visual C。
  • UNIX:标识当前系统是否为Unix系列(包括Linux,Cygwin和Apple)。
  • WIN32:标识当前系统是否为Windows及Win64。



不必担心这里介绍的命令和变量太多,也不必担心它们会很快消失在您的脑海深处。下一节我们将尝试创建一个稍微复杂一些的工程VersionMe,并争取将上文涉及到的大部分命令和内置变量派上用场,以求在实战中让您领略到CMake的强大魅力。

不过在结束枯燥的本章之前,我们还需要介绍一下CMake中重要的条件语句语法,循环语句语法和宏函数。它们分别相当于C程序中的if…else,while/for以及函数的作用,并且条件和循环语句都可以嵌套工作。毫无疑问,它们在脚本语言的流程控制过程中必然不可或缺。

CMake中的条件语句基本格式为:

  1. IF( expression )
  2. ELSE( expression )
  3. ENDIF( expression )
复制代码

或者,

  1. IF( expression1 )
  2. ELSEIF( expression2 )
  3. ELSE()
  4. ENDIF()
复制代码

这里的expression是判断条件,和C/C++类似,CMake的条件也存在“与/或/非”以及“等于/大于/小于”等几种操作符,分别用AND/OR/NOT以及EQUAL/LESS/GREATER来表示。当判断条件为真,执行IF后的命令段,否则继续判断并执行相应条件对应的命令段,或者不执行任何操作。例如:

  1. IF ( ${number} GREATER 4 )
  2. ENDIF( ${number}GREATER 4 )
复制代码

表示判断变量number是否大于4,进而执行对应的语句段。此时用户定义的字符串变量会被自动转换为整型变量以便进行判断。
此外形同这样的判断语句也是十分常见的:

  1. IF ( NOT ${variable} )
  2. ENDIF( NOT ${variable} )
复制代码

如果变量variable的值为空,0,N,NO,OFF,FALSE,NOTFOUND这几种之一的话,则认为此变量表示“假”,即此处的“NOT ${variable}”为真。

CMake中的循环语句基本格式为:

  1. FOREACH( var arg1 arg2 … )
  2. ENDFOREACH( var )
复制代码

这里设置一个循环的局部变量var,每次将其赋为arg1,arg2等变量(或者变量数组)中的一个值,并执行循环中的命令段。例如:

FOREACH( var ${OPENGL_LIBRARIES} ),它表示将局部变量var每次设置为变量OPENGL_LIBRARIES中的一个值。后者的内容可能为“opengl32.lib;glu32.lib”的形式。

CMake自动将分号分隔的字符串认为是数组,因此会自动从该变量中择取var的取值。

另一种表达循环语句的语法格式为:

  1. WHILE ( expression )
  2. ENDWHILE( expression )
复制代码

这里的expression和IF语句判断字段中的含义相同。

CMake中的宏函数可以理解为C语言的函数,它改变代码执行跳转的流程并简化了脚本程序的开发,其基本格式为:

  1. MACRO( funcname [arg1 [arg2 …]] )
  2. ENDMACRO( funcname )
复制代码

和函数的编写要求一样,CMake的宏函数必须指定一个函数名funcname,以及零个或多个输入参数arg1,arg2等。需要调用宏函数的时候,只要直接使用funcname(arg1 arg2)的形式就可以了,例如:

  1. MACRO( MY_FUNC arg1 arg2 )
  2. ENDMACRO( MY_FUNC )
复制代码

在主程序中,需要调用此宏函数时,只需执行形同下面的语句:

  1. MYFUNC( param1 param2 )
复制代码

就可以将实际参数param1和param2传入宏函数体。

此外,宏函数体内可以使用内置变量${ARGC},${ARGV}和${ARGN}来表达传入参数的属性:${ARGC}保存了传入参数的个数;${ARGV}保存一个传入参数组成的数组,可以供FOREACH语句使用;${ARGN}则比较特殊,它保存了“显式参数”之外的所有“隐式参数”所组成的数组。对于上面的例句来说,arg1,arg2就是显式参数,而如果用户在调用MY_FUNC时采用下面的形式:

  1. MYFUNC( param1 param2 other1 other2 … )
复制代码

那么other1,other2等就是隐式参数,可以用${ARGN}来获取它们的数组。这对于CMake而言是完全合法的,并且可以因此定义不定参数项的宏函数,从而大大增强了脚本程序的灵活性。

 

 

1.5 VersionMe工程设计

下面我们将着手设计一个稍微复杂一些的工程,名为VersionMe。它同样包括一个动态链接库工程和一个依赖于它的可执行工程,并且计划实现以下一些功能:
  • 可以查询当前操作系统的版本;
  • 可以查询系统中安装的OpenSceneGraph库的版本;
  • 可以执行上一次我们创建的HelloWorld工程的方法;

为了使我们的工程更具备开源工程的特征,我们模仿OpenSceneGraph的文件夹结构,设计如下:


名为VersionLib的链接库工程的头文件和源代码文件将分别保存到include和src两个目录下;而名为test的可执行工程则单独保存在同名目录下;此外,CMakeModules目录专职负责保存相关的CMake脚本数据。

这个工程的重点毫无疑问是VersionLib链接库,它的工作是提供显示系统版本和OSG版本的函数,以及调用之前的Hello::sayHello方法,再次在控制台界面上显示“Hello CMake!”这一行简单而友好的欢迎文字。

VersionLib库包括一个头文件Version和一个源代码文件Version.cpp,其代码如下:
  1. /* Version */
  2. #ifndef H_VERSION
  3. #define H_VERSION
  4. #if defined(_MSC_VER) || defined(__CYGWIN__) || defined(__MINGW32__)
  5. #    ifdef VERSIONME_LIBRARY
  6. #        define VERSIONME_EXPORT __declspec(dllexport)
  7. #    else
  8. #        define VERSIONME_EXPORT __declspec(dllimport)
  9. #    endif
  10. #else
  11. #    define VERSIONME_EXPORT
  12. #endif
  13. class VERSIONME_EXPORT Version
  14. {
  15. public:
  16.     void printHello();
  17.     const char* systemVersion();
  18.     const char* osgVersion();
  19. };
  20. #endif
  21. /* Version.cpp */
  22. #ifdef _MSC_VER
  23. #    include <windows.h>
  24. #else
  25. #    include <sys/utsname.h>
  26. #endif
  27. #include <sstream>
  28. #include <HelloLib/Hello>
  29. #include <osg/Version>
  30. #include <VersionLib/Version>
  31. static char g_version[255] = "";
  32. void Version::printHello()
  33. {
  34.     Hello hello;
  35.     hello.sayHello();
  36. }
  37. const char* Version::systemVersion()
  38. {
  39.     std::stringstream stream;
  40. #ifdef _MSC_VER
  41.     OSVERSIONINFO osvi;
  42.     ZeroMemory( &osvi, sizeof(OSVERSIONINFO) );
  43.     osvi.dwOSVersionInfoSize = sizeof( OSVERSIONINFO );
  44.     GetVersionEx( &osvi );
  45.     stream << "Windows-";
  46.     stream << osvi.dwMajorVersion << "." << osvi.dwMinorVersion << "."
  47.           << osvi.dwBuildNumber;
  48. #else
  49.     struct utsname buff;
  50.     uname( &buff );
  51.     stream << buff.sysname << "-" << buff.release;
  52. #endif
  53.     stream >> g_version;
  54.     return g_version;
  55. }
  56. const char* Version::osgVersion()
  57. {
  58.     return osgGetVersion();
  59. }
复制代码
具体的实现过程我们不作介绍,这里的osgGetVersion()是OSG的版本查询函数,而GetVersionEx()和uname则分别是Windows和Linux下的版本查询API函数。嗯,是否觉得这里Windows API的实现更加费劲呢?(^_^)

此外,这里我们引用头文件Hello并调用了其中的函数,因此在之后的CMake脚本设计过程中,要考虑正确搜索和指定Hello头文件依赖目录以及依赖库的路径位置。

而Test工程的源代码就很简单了,只有一个test.cpp文件,内容如下:
  1. /* test.cpp */
  2. #include <iostream>
  3. #include <VersionLib/Version>
  4. int main( int argc, char** argv )
  5. {
  6.     Version obj;
  7.     std::cout << "Operating System Version: " << obj.systemVersion() << std::endl;
  8.     std::cout << "OpenSceneGraph Version: " << obj.osgVersion() << std::endl;
  9.     obj.printHello();
  10.     return 0;
  11. }
复制代码
我们希望在CMake-GUI窗口中有一个用户选项,用来决定是否编译这个Test工程。另外,我们还希望能够使用类似“make install”这样的命令来安装所有工程相关的动态库,静态库,头文件和可执行文件。(之前的HelloWorld工程是无法这样做的)

以上就是VersionMe工程设计的所有要求。下面,我们将开始CMake脚本的设计。
位于VersionMe根目录下的CMakeLists.txt脚本如下:
  1. PROJECT( VersionMe )
  2. CMAKE_MINIMUM_REQUIRED( VERSION 2.4.7 )
  3. IF( COMMAND CMAKE_POLICY )
  4.    CMAKE_POLICY( SET CMP0003 NEW )
  5. ENDIF( COMMAND CMAKE_POLICY )
  6. SET( CMAKE_MODULE_PATH
  7.      "${PROJECT_SOURCE_DIR}/CMakeModules/;${CMAKE_MODULE_PATH}" )
  8. SET( CMAKE_DEBUG_POSTFIX "d"
  9.      CACHE STRING "add a postfix, usually d on windows" )
  10. OPTION( BUILD_TEST "Set to ON to build the test application." ON )
  11. IF( WIN32 )
  12.     IF( MSVC )
  13.         ADD_DEFINITIONS( -D_SCL_SECURE_NO_WARNINGS )
  14.         ADD_DEFINITIONS( -D_CRT_SECURE_NO_DEPRECATE )
  15.     ENDIF( MSVC )
  16. ENDIF( WIN32 )
  17. FIND_PACKAGE( Hello )
  18. FIND_PACKAGE( osg )
  19. INCLUDE( CustomModules )
  20. INCLUDE_DIRECTORIES( include )
  21. ADD_SUBDIRECTORY( src/VersionLib )
  22. IF( BUILD_TEST )
  23.     ADD_SUBDIRECTORY( test )
  24. ENDIF( BUILD_TEST )
复制代码
这一次的代码编写恐怕比之前丰富了不少,有点眼晕吗?没关系,我们结合上一节的CMake命令和内置变量介绍,一点一点地分析。

首先设置工程名称和所需的CMake版本,此处还使用了一个CMAKE_POLICY命令来设置版本的兼容性,以避免一些不必要的警告信息。

之后,我们指定基本的CMake配置参数,包括模块搜索目录,Debug版本生成目标的后缀,一个用户选项BUILD_TEST,以及针对Visual Studio的一些宏定义设置。(通过判断WIN32和MSVC内置变量来执行这一动作)

后面的工作比较重要一些,首先是使用FIND_PACKAGE来搜索外部依赖库的路径。CMake中预置了很多模块搜索的脚本,可以用来检索各种知名工程的文件和路径信息,并以CMake变量的方式返回。例如,FIND_PACKAGE(OpenGL)将自动调用CMake安装目录share/cmake-2.6/Modules文件夹中的FindOpenGL.cmake脚本的内容,自动搜索或者由用户指定OpenGL头文件和链接库的位置,并主要返回以下变量:
  • OPENGL_FOUND:是否找到OpenGL库的标识;
  • OPENGL_GLU_FOUND:是否找到GLU库的标识;
  • OPENGL_INCLUDE_DIR:头文件gl.h所在的文件夹路径;
  • OPENGL_gl_LIBRARY:链接库opengl32的绝对路径;
  • OPENGL_glu_LIBRARY:链接库glu32的绝对路径;

这个时候可以用下面的形式为名为ProjName的工程指定依赖库OpenGL的头文件和链接库文件,脚本代码如下:
  1. INCLUDE_DIRECTORIES( ${OPENGL_INCLUDE_DIR} )
  2. TARGET_LINK_LIBRARIES( ProjName
  3.                          ${OPENGL_gl_LIBRARY}
  4.                          ${OPENGL_glu_LIBRARY}
  5. )
复制代码
注意Windows环境下不存在OPENGL_INCLUDE_DIR,因为没有必要做特别指定。

回到我们的VersionMe工程来,这里我们试图搜索两个外部依赖库的信息:Hello代表我们之前生成的HelloWorld工程,而OSG表示外部的OpenSceneGraph工程。后者的搜索模块在CMake的较新版本中已经包含,包括Findosg,FindosgUtil,FindosgViewer等多个cmake文件,可以使用FIND_PACKAGE直接调用并返回相应的头文件和链接库路径变量。(通常名为OSG_INCLUDE_DIR,OSG_LIBRARY等)

但是Hello这个库却不可能有相应的模块文件,毕竟CMake可不知道我们之前做了什么。因此我们需要手动编写名为FindHello.cmake的脚本,并将其保存在CMakeModules目录下。具体的脚本编写方法我们稍后再说。

在使用FIND_PACKAGE搜索到外部Hello库和OSG库的具体位置之后,(也可能找不到,此时需要在CMake-GUI中手动进行设置),下一步还要使用INCLUDE命令包含一个自定模块CustomModules.cmake。它的工作是定义了几个方便VersionMe工程设置的宏函数,以免我们在后面的子工程创建过程中编写过多的代码;OSG中有一个类似功能的模块文件OsgMacroUtils.cmake,有兴趣的朋友不妨提前阅读一下。

笔者在CustomModules中手动编写了三个实用的宏函数:INCLUDE_FOR_PROJECT,LINK_PROJECT以及INSTALL_PROJECT。它们不仅将在VersionMe的设计中大显身手,也许更可以为您自己的工程脚本编写提供一些助力。

在INCLUDE命令之后,注意我们还使用了一次INCLUDE_DIRECTORIES命令。这是什么意思呢?再次考虑VersionMe的文件夹结构,和之前的HelloWorld工程不同,这一次所有的头文件不再与源代码文件放在同一目录下,而是放置在单独的include文件夹,以便于更有效的文件管理。但是,对于编译器而言,这样会使它无法正确搜索到#include引用的头文件位置。因此我们必须提前设置好相应的头文件依赖路径,并且这一设置对于所有的子工程都是统一的。

仅对于上例而言,下面两种指定头文件路径的方式是等价的:
  1. INCLUDE_DIRECTORIES( include )
  2. INCLUDE_DIRECTORIES( “${PROJECT_SOURCE_DIR}/include” )
复制代码
这之后的脚本代码就轻松一点了,ADD_SUBDIRECTORY命令指定了下一级子工程CMake脚本的位置,此外我们还通过判断用户是否选择了BUILD_TEST选项,来决定是否编译test工程。(即是否进入test工程所在的脚本目录)

我们快速浏览一下VersionLib子工程和test子工程的脚本代码:
  1. SET( LIB_NAME VersionLib )
  2. SET( HEADER_PATH ${PROJECT_SOURCE_DIR}/include/${LIB_NAME} )
  3. SET( ${LIB_NAME}_HEADERS
  4.      ${HEADER_PATH}/Version
  5. )
  6. SET( ${LIB_NAME}_SOURCES
  7.      Version.cpp
  8. )
  9. ADD_DEFINITIONS( -DVERSIONME_LIBRARY )
  10. ADD_LIBRARY( ${LIB_NAME} SHARED
  11.                ${${LIB_NAME}_HEADERS}
  12.                ${${LIB_NAME}_SOURCES}
  13. )
  14. SET_TARGET_PROPERTIES( ${LIB_NAME}
  15.                           PROPERTIES DEBUG_POSTFIX
  16.                           "${CMAKE_DEBUG_POSTFIX}"
  17. )
  18. INCLUDE_FOR_PROJECT( ${LIB_NAME} HELLO OSG )
  19. LINK_PROJECT( ${LIB_NAME} HELLO OSG )
  20. INSTALL_PROJECT( ${LIB_NAME} )
复制代码
虽然看上去复杂了一些,但其实源文件设置和工程属性设置的脚本代码并没有太多深奥的地方。无非是大量使用CMake变量而已,如果您熟悉其它程序语言设计的话,相信很快会对此习以为常,并能够适当地在自己的工程中加以借鉴。当使用ADD_LIBRARY新增了一个动态链接库工程之后,我们还额外地使用SET_TARGET_PROPERTIES设置了工程Debug版本的生成目标后缀——它有什么作用呢?还记得您编译OSG的时候,会生成osgd,osgDBd,osgViewerd等一系列有别于Release版本文件的链接库吧,这里就是同样的道理,这个命令将为VersionLib库的Debug版本文件新增一个用于区分的后缀,使之成为VersionLibd.dll的形式,要知道编译器自己可不会这样做。

之后我们依次调用三个自定义的宏函数,也就是前文说过的INCLUDE_FOR_PROJECT,LINK_PROJECT和INSTALL_PROJECT。它们的工作依次是:指定工程所用的头文件依赖目录(Hello库和OSG库的头文件目录);工程所需的外部链接库文件绝对路径(HelloLib.lib和osg.lib);以及指定安装这个工程的相关生成文件到预设的目录中。

这些宏函数的具体实现过程稍后详述。

子工程test的脚本代码为:
  1. SET( EXAMPLE_NAME Test )
  2. SET( EXAMPLE_FILES
  3.     test.cpp
  4. )
  5. ADD_EXECUTABLE( ${EXAMPLE_NAME} test.cpp )
  6. SET_TARGET_PROPERTIES( ${EXAMPLE_NAME}
  7.                           PROPERTIES DEBUG_POSTFIX
  8.                           "${CMAKE_DEBUG_POSTFIX}" )
  9. LINK_PROJECT( ${EXAMPLE_NAME} VersionLib )
  10. INSTALL_PROJECT( ${EXAMPLE_NAME} )
复制代码
注意我们没有必要为Test工程设置头文件依赖目录了,从test.cpp的源代码可以看出,它只需要引用VersionLib/Version这个头文件,(以及一个可以由编译器自动找到的STL头文件iostream)而这个文件的路径位于:${PROJECT_SOURCE_DIR}/include,毫无疑问,它在前文中已经由INCLUDE_DIRECTORIES设置完毕了。

Test子工程依赖于即将生成的VersionLib库,不必担心它们的编译顺序上会有什么偏差,CMake会为我们安排好一切
那么,是时候启动CMake-GUI,设置好所需的选项和路径,然后开始编译VersionMe工程,观察输出结果,并结束这一篇“规模宏大”的教程了……不,先等一下,我们是否忘记什么了?
用于搜索HelloLib库的FindHello.cmake,它是怎样编写的?

用于实现实用宏函数的CustomModules.cmake,它又是怎样编写的?

是的,看来我们之前欠下的债务还没有完全还清啊,那么,首先我们来解析一下:如何编写脚本,搜索一个外部依赖库的信息并作为CMake变量传递给当前工程。
  1. FIND_PATH( HELLO_INCLUDE_DIR HelloLib/Hello
  2.   PATHS
  3.   $ENV{PATH}
  4.   /usr/include/
  5.   /usr/local/include/
  6. )
  7. FIND_LIBRARY( HELLO_LIBRARY
  8.   NAMES HelloLib
  9.   PATHS
  10.   $ENV{PATH}
  11.   /usr/lib
  12.   /usr/local/lib
  13. )
复制代码
由于HelloLib是我们之前的章节中实现的简单工程,因此我们也很清楚,使用它需要一些什么工作:找到一个头文件Hello,以及找到一个静态链接库文件HelloLib.lib(或者libHelloLib.so)。为了让之后的各个子工程都能够正确识别这些文件的位置,我们使用两个变量HELLO_INCLUDE_DIR和HELLO_LIBRARY返回搜索到的信息,并且这样的命名方式也符合其它CMake搜索模块的一贯命名原则,以及最为重要的,符合我们自定义的宏函数的执行需要!

按照FIND_PATH和FIND_LIBRARY的语法可知,系统将自动在PATHS指定的目录中搜索HelloLib/Hello文件和HelloLib文件的位置,并将结果返回到变量中;如果不能找到,那么CMake-GUI中会自动出现红色的选项栏,允许用户自己选择文件的位置,以便正确执行后面的编译工作。

这里还出现了一个特殊的格式“$ENV{PATH}”,它表示搜索系统环境变量PATH中定义的所有路径,并且可以将PATH替换为别的环境变量名称;此外我们还可以使用形如“[HKEY_LOCAL_MACHINE\\...]”的字样,顾名思义,它指定搜索Windows注册表相应位置保存的路径信息,当然这样的写法在Unix/Linux系统中没有效果。

而对于宏函数的实现,我们在这里只介绍INCLUDE_FOR_PROJECT的写法,并期望读者朋友能够举一反三,自行阅读和实验其它两个宏函数的内容,理解和更新它们的用法,进而设计出属于您自己的更加丰富多彩的CMake脚本来。宏函数的内容如下:
  1. MACRO( INCLUDE_FOR_PROJECT PROJNAME )
  2.     FOREACH( varname ${ARGN} )
  3.         IF( ${varname}_INCLUDE_DIR )
  4.             INCLUDE_DIRECTORIES( "${${varname}_INCLUDE_DIR}" )
  5.         ELSE( ${varname}_INCLUDE_DIR )
  6.             INCLUDE_DIRECTORIES( "${varname}" )
  7.         ENDIF( ${varname}_INCLUDE_DIR )
  8.     ENDFOREACH( varname )
  9. ENDMACRO( INCLUDE_FOR_PROJECT PROJNAME )
复制代码
如我们在上一节介绍的那样,宏函数INCLUDE_FOR_PROJECT包含了至少一个输入参数PROJNAME,但事实上它并没有在函数体中发挥作用,而是仅仅用来标示当前调用宏函数的工程名称。此外,有关${ARGN}的意义,也请大家回顾一下之前的内容,它的作用是排列所有“隐式输入”的参数,例如:
  1. INCLUDE_FOR_PROJECT( VersionLib HELLO OSG )
复制代码
这一函数调用传入的实际参数,${PROJNAME}对应“VersionLib”,而${ARGN}对应“HELLO;OSG”这个字符串数组。

FOREACH命令将依次取出HELLO和OSG字符串,并赋值到变量${varname}中。这是我们运用了刚刚提到过的一个命名惯例:对于使用FIND_PATH搜索头文件的返回变量,通常命名为“…_INCLUDE_DIR”,而使用FIND_LIBRARY搜索库文件的结果变量命名为“..._LIBRARY”,因此,现在我们直接尝试查找用户变量${varname}_INCLUDE_DIR,也就是HELLO_INCLUDE_DIR和OSG_INCLUDE_DIR,并取出其中的结果。

如果这个环境变量存在,那么使用INCLUDE_DIRECTORIES将其中的路径信息传递给子工程;否则的话,直接将变量${varname}本身传递给子工程,换句话说,对于这个宏函树来说,下面的调用形式:
  1. INCLUDE_FOR_PROJECT( VersionLib “E:/Projects/HelloWorld” )
复制代码
也是可以的。它将直接转换为INCLUDE_DIRECTORIES(“E:/Projects/HelloWorld”)的形式。

完成后的CMake-GUI界面如下,注意笔者使用了“out-of-source”的编译方式,并且设置了与源代码目录不同的安装目录:

最后的编译生成和执行结果如下。怎么样,是否觉得这个其实再简单不过的工程突然变得十分专业了呢?您的工程也可以如此!

1.6 参考资料

好了,本以为只是《你所不知的OSG》中普普通通的一章,却不料其内容远远超出了笔者的预计,不知是否也远远超出了您对于冗长文章的忍耐能力?(^_^)不过这依然只是管中窥豹,见一斑而已——CMake网站上已经提供了最新教材书籍的购买方式,包括《Mastering CMake》等四本相关图书已经出版,而另有四本正紧锣密鼓地筹划之中……怎么样,光是出版物的规模就已经如此宏大。您是否要惊叹一下,这小小的“辅助编译生成系统”,实际上却大有文章呢?

本文所述的内容只是最浅显的CMake应用,对于命令语法的解释非常不全面。并且为了保持文章的“初级性”,删去了不少高级用户才可能用到的指令和枚举参数,以及正则表达式,数组赋值,字符串规则等一系列深入有趣却难以再纳入本文篇幅的内容。如是种种,还需要读者朋友自行钻研发掘,并帮助其发展壮大。

以下列出了可用的CMake学习参考资料网址。

 

你可能感兴趣的:(Make)