CMake 学习笔记

引子
Make or CMake,What's different?
CMake简介
[附]CMake常用指令

引子


人们为了解决一个问题,发明了一个工具,然后这个工具产生了N个问题;

为解决工具产生的N个问题,人们又发明了N个不同的工具。

笔者平素接触了不少 Unix-like 下的各类 Open-Source 项目、工具,早已习惯了机械地重复如下过程:

./bootstrap
make
make install
打完收工!

想必很多人也跟我一样仅是盲目跟随操作说明,而鲜于问问自己:

这些操作背后都具体做了哪些事?
构建的整个过程是什么?
为什么要用这种方式去组织?
CMake 和 Make 之间有什么区别?
这有关系吗?他们可以互相转换吗?

其实很早就萌生了深入了解 makefile 的念头,奈何早已甘饴了各类 IDE 的畅便,加之 make、CMake 长长的操作手册、命令说明,看着一个头变两个大。然 “不积跬步,无以至千里;不积小流,无以成江河。” 非克不克不及极客也~!

那么,问题来了

Make or CMake,What's different?


Make

Make 是什么

要设计一个软件系统,我们首先编写源码,然后通过编译器编译和创建可执行文件。可执行文件就是要实现最终功能的文件。“Make”是一个工具,它控制可执行程序和程序源文件中非源码文件的生成

“Make”工具需要清楚的知道如何构建程序。 它通过一个叫做“makefile”的文件知晓如何构建你的程序。这个文件列出了所有的非源码文件以及如何由别的文件来计算它。当你编写了一个程序,你应该为它写一个makefile文件,这样才有可能通过使用“Make”来构建和安装你的程序。 很简单的事情。如果你不理解的话,多读几遍这一段文字,因为理解这一段文字对于接下来的篇幅很重要。

为什么需要“Make”

首先,它可以使得终端用户构建和安装你的应用包,而不用去详细的了解它具体是如何做到的。每一个工程都有它自己的规则和细微的差别,这会使得每次在复用的时候会变得很痛苦。这就是我们创建这个makefile文件的原因。 构建步骤精确的记录在你提供的这个makefile文件中。“Make” 当源码文件发生变化时自动的指出哪一个文件需要更新。 同时,当一个非源码文件依赖的另一个非源码文件发生改变时,它也自动确定以适当的顺序进行更新文件。

其次,每次当我们改变了系统中的一小部分源码的时候,重新编译整个程序的效率是很低的。因此,当我们改变了一小部分的源码文件的时候重新执行“Make”,它将不会重新编译整个程序。它仅仅更新那些直接或者间接依赖这些改变了的源码文件的非源码文件。

很酷吧!“Make” 不局限于具体的语言。对于程序中的每一个非源码文件,makefile文件详细的说明了执行需要的shell命令。这些shell命令能够启动编译器产生目标文件,链接器产生可执行文件、更新库、镜像生成器格式化文档,等等。Make不仅仅局限于构建一个包,你也可以安装或者卸载一个包,生成索引表 或者其他一些你经常做的值得你写下来怎么去做的事情。

废话了那么多,简而言之就是自动化项目编链过程,及控制生成最终的目标文件。

CMake - "cross platform make"

相较 make 而言

CMake 支持跨平台 Make。 如果你不知道使用何种编译器,你不能使用相同的编译器去编译所有不同种类的源码。你可以手动的指用何种编译器,但是这将变得繁琐和痛苦。CMake 辨别使用哪种编译器去编译给出的源码种类,为每一种类型的目标平台按照正确的顺序调用命令。因此,将有很多非显式的命令,比如$(CC)。

如果你是代码强迫症,请继续往下读。如果你不喜欢这一切,你可以跳过这一部分。

一般的 编译/链接标识 处理头文件、库文件、以及重定位其他平台无关和构建系统独立命令。调试标识被包含,通过设置变量 CMAKE_BUILD_TYPE 为“debug”,或者在调用程序时传递给CMake:cmake -DCMAKE——BUILD——TYPE:STRING=Debug

CMake 也提供平台无关的包含,通过‘-fPIC’标志(POSITION_INDEPENDENT_CODE属性)。因此,更多隐式的设置能够在 CMake 命令中实现,在 makefile 文件中也可以通过使用 COMPILE_FLAGS 或者相关的属性。当然,CMake在集成第三方库(像OpenGL)方面也变得更加轻便。你可以自己做所有的跨平台工作,但是这将花费很多时间。

CMake解决了这个问题,但是与此同时,它比GNU构建系统更有优势:

  • 用于编写CMakeLists.txt文件的语言具有可读性和很容易理解。
  • 不仅可以使用“Make” 来构建工程。
  • 支持多种生产工具,比如Xcode, Eclipse, Visual Studio, etc.

CMake与Make对比具有以下优点:

  • 自动发现跨平台系统库。
  • 自动发现和管理的工具集。
  • 更容易将文件编译进共享库, 以一种平台无关的方式或者以比make更容易使用的的生成方式。

CMake不仅仅只“make”,所以它变得更复杂。从长远来看,最好能够学会使用它。如果你仅仅在一个平台上构建小的工程,“Make”更适合完成这部分工作。

下面主要介绍 CMake

CMake简介


如何使用 CMake

基本操作流程为:

$> ccmake directory ## 配置编译选项,如项目目录,一般这步可省略。
$> cmake directory  ## 根据 CMakeLists.txt 生成 Makefile 文件
$> make             ## make 命令

CMake 的执行就是这么简单,其难点在于如何编写 CMakeLists.txt 文件。

CMakelists.txt

CMake 的所有的语句都写在一个叫 CMakeLists.txt 的文件中。当 CMakeLists.txt 文件确定后,可以用 ccmake 命令对相关 的变量值进行配置。这个命令必须指向 CMakeLists.txt 所在的目录。配置完成之后,应用 cmake 命令生成相应的 makefile(在Unix like系统下) 或者 project文件(指定用window下的相应编程工具编译时)

cmake 要求工程 主目录 和所有存放 源代码子目录 下都要编写 CMakeLists.txt 文件,注意大小写.

参考官网的 Tutorials https://cmake.org/examples/ ,下面通过几个示例,由浅入深介绍如何编写。

CMakelists.txt step by step

Sample 1.

简单目录结构如下:

.
├── CMakelists.txt
├── include
│   └── pow.h
└── src
    ├── main.c
    └── pow.c

2 directories, 4 files

pow.h

#ifndef POW_H
#define POW_H

#include <math.h>

double calc_pow(double x, double y);

#endif

pow.c

#include "pow.h"

double calc_pow(double x, double y) {
    return pow(x, y);
}

main.c

#include <stdlib.h>
#include <stdio.h>
#include "../include/pow.h"

int main(int argc, char const *argv[])
{
    if (argc < 3) {
        printf("Error arg count!\n");
        return -1;
    }

    double result = calc_pow( atof(argv[1]), atof(argv[2]) );
    printf("pow result = %f\n", result);

    return 0;
}

CMakelists.txt (命令具体说明参见附录)

## 指定允许执行的最低 CMake 版本要求;不写会出警告
cmake_minimum_required( VERSION 2.8 )

## 指定项目的名称,通常对应于项目文件夹名
project( SAMPLE_SINGLE_DIR )

## 指定引用目录
include_directories( include )

## 自动构建源文件目录
aux_source_directory( src DIR_SRCS )

## 指定编译目标文件
add_executable( demo ${DIR_SRCS} )

## 添加链接库,相同于指定-l参数(此处没有用到)
#TARGET_LINK_LIBRARIES(${PROJECT_NAME} m) #比如我们用到了libm.so(命名规则:lib+name+.so

执行编译:

XXOO:smaple-1$ mkdir build
XXOO:smaple-1$ cd build/
XXOO:build$ cmake ..
......
-- Build files have been written to: /Users/XXOO/Documents/cmake-samples/smaple-1/build
XXOO:build$ make 
Scanning dependencies of target demo
[ 33%] Building C object CMakeFiles/demo.dir/src/main.c.o
[ 66%] Building C object CMakeFiles/demo.dir/src/pow.c.o
[100%] Linking C executable demo
[100%] Built target demo

XXOO:build$ ls
CMakeCache.txt    CMakeFiles    Makefile    cmake_install.cmake    demo

检验:

XXOO:build$ ./demo
Error arg count!
XXOO:build$ ./demo 2 3
pow result = 8.000000
XXOO:build$ ./demo 3 2
pow result = 9.000000

本来想弄个复杂点的例子,奈何篇幅太长了,就此打住,以后有时间再补上。

本人也是在学习中,难免出现错漏。╮(╯▽╰)╭ 记下来主要是方便自己查阅和总结。

[附]CMake常用指令


[按]out-of-source build

一般在源文件的顶层目录中新建 build 目录。

对于命令行可以:cd build

然后:cmake .. -G"MinGW Makefiles"即可。

这样所有的临时文件都会放在 build 目录下不会和 source 有任何的瓜噶。

in-source buildout-of-source build相对,指是否在CMakeLists.txt所在目录进行编译。

自定义变量

主要有隐式定义和显式定义两种。

隐式定义的一个例子是PROJECT指令,它会隐式的定义 < projectname >_BINARY_DIR< projectname >_SOURCE_DIR 两个变量。

显式定义使用SET指令构建自定义变量,比如: SET(HELLO_SRCmain.c) 就可以通过 ${HELLO_SRC}来引用这个自定义变量了。

变量引用方式

使用${}进行变量的引用;在IF等语句中,是直接使用变量名而不通过${}取值。

常用变量

CMAKE_BINARY_DIR 
PROJECT_BINARY_DIR 
< projectname >_BINARY_DIR 

这三个变量指代的内容是一致的,如果是in-source编译,指得就是工程顶层目录;

如果是out-of-source编译,指的是工程编译发生的目录。PROJECT_BINARY_DIR跟其它指令稍有区别,目前可以认为它们是一致的。

CMAKE_SOURCE_DIR 
PROJECT_SOURCE_DIR 
< projectname >_SOURCE_DIR

这三个变量指代的内容是一致的,不论采用何种编译方式,都是工程顶层目录。也就是在in-source编译时,他跟CMAKE_BINARY_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 <新路径>)并不会对这个变量造成影响,它仅仅修改了最终目标文件存放的路径。

CMAKE_CURRENT_LIST_FILE
输出调用这个变量的CMakeLists.txt的完整路径

CMAKE_CURRENT_LIST_LINE
输出这个变量所在的行

CMAKE_MODULE_PATH
这个变量用来定义自己的cmake模块所在的路径。如果工程比较复杂,有可能会自己编写一些cmake模块,这些cmake模块是随工程发布的,为了让cmake在处理CMakeLists.txt时找到这些模块,你需要通过SET指令将cmake模块路径设置一下。比如SET(CMAKE_MODULE_PATH,${PROJECT_SOURCE_DIR}/cmake)
这时候就可以通过INCLUDE指令来调用自己的模块了。

EXECUTABLE_OUTPUT_PATH
新定义最终结果的存放目录

LIBRARY_OUTPUT_PATH
新定义最终结果的存放目录

PROJECT_NAME
返回通过PROJECT指令定义的项目名称。

CMake调用环境变量的方式

使用$ENV{NAME}指令就可以调用系统的环境变量了。比如: MESSAGE(STATUS "HOME dir: $ENV{HOME}")

设置环境变量的方式是SET(ENV{变量名} 值)

CMAKE_INCLUDE_CURRENT_DIR
自动添加CMAKE_CURRENT_BINARY_DIR和CMAKE_CURRENT_SOURCE_DIR到当前处理的CMakeLists.txt,相当于在每个CMakeLists.txt加入:INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR})

CMAKE_INCLUDE_DIRECTORIES_PROJECT_BEFORE
将工程提供的头文件目录始终置于系统头文件目录的前面,当定义的头文件确实跟系统发生冲突时可以提供一些帮助。

CMAKE_INCLUDE_PATH
配合 FIND_FILE() 以及 FIND_PATH() 使用。如果头文件没有存放在常规路径(/usr/include, /usr/local/include等),则可以通过这些变量就行弥补。如果不使用 FIND_FILE 和 FIND_PATH的话,CMAKE_INCLUDE_PATH,没有任何作用。

CMAKE_LIBRARY_PATH
配合 FIND_LIBRARY() 使用。否则没有任何作用。

系统信息

  • CMAKE_MAJOR_VERSION,CMAKE主版本号,比如2.4.6中的2
  • CMAKE_MINOR_VERSION,CMAKE次版本号,比如2.4.6中的4
  • CMAKE_PATCH_VERSION,CMAKE补丁等级,比如2.4.6中的6
  • CMAKE_SYSTEM,系统名称,比如Linux-2.6.22
  • CMAKE_SYSTEM_NAME,不包含版本的系统名,比如Linux
  • CMAKE_SYSTEM_VERSION,系统版本,比如2.6.22
  • CMAKE_SYSTEM_PROCESSOR,处理器名称,比如i686
  • UNIX,在所有的类Unix平台为TRUE,包括OSX和cygwin
  • WIN32,在所有的Win32平台为TRUE,包括cygwin

主要的开关选项

CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS
用来控制IF ELSE语句的书写方式。

BUILD_SHARED_LIBS
这个开关用来控制默认的库编译方式。如果不进行设置,使用ADD_LIBRARY并没有指定库类型的情况下,默认编译生成的库都是静态库;如果SET(BUILD_SHARED_LIBSON)后,默认生成的为动态库。

CMAKE_C_FLAGS
设置C编译选项,也可以通过指令ADD_DEFINITIONS()添加。

MAKE_CXX_FLAGS
设置C++编译选项,也可以通过指令ADD_DEFINITIONS()添加。

基本指令

PROJECT(HELLO)
指定项目名称,生成的VC项目的名称,使用${HELLO_SOURCE_DIR}表示项目根目录。

INCLUDE_DIRECTORIES
指定头文件的搜索路径,相当于指定gcc的-I参数

INCLUDE_DIRECTORIES(${HELLO_SOURCE_DIR}/Hello)

增加Hello为include目录

TARGET_LINK_LIBRARIES
添加链接库,相同于指定-l参数

TARGET_LINK_LIBRARIES(demoHello)

将可执行文件与Hello连接成最终文件demo

LINK_DIRECTORIES
动态链接库或静态链接库的搜索路径,相当于gcc的-L参数

LINK_DIRECTORIES(${HELLO_BINARY_DIR}/Hello)
增加Hello为link目录

ADD_DEFINITIONS
向C/C++编译器添加-D定义,比如:

ADD_DEFINITIONS(-DENABLE_DEBUG-DABC)
参数之间用空格分割。如果代码中定义了:

#ifdef ENABLE_DEBUG

#endif

这个代码块就会生效。如果要添加其他的编译器开关,可以通过CMAKE_C_FLAGS变量和CMAKE_CXX_FLAGS变量设置。

ADD_DEPENDENCIES*
定义target依赖的其它target,确保在编译本target之前,其它的target已经被构建。ADD_DEPENDENCIES(target-name depend-target1 depend-target2 ...)

ADD_EXECUTABLE(helloDemo demo.cxx demo_b.cxx)
指定编译,好像也可以添加.o文件,将cxx编译成可执行文件

ADD_LIBRARY(Hellohello.cxx) #将hello.cxx编译成静态库如libHello.a

ADD_SUBDIRECTORY(Hello) #包含子目录

ENABLE_TESTING 指令用来控制Makefile是否构建test目标,涉及工程所有目录。语法很简单,没有任何参数,

ENABLE_TESTING() 一般放在工程的主CMakeLists.txt中。

ADD_TEST(testnameExename arg1 arg2 …) testname是自定义的test名称,Exename可以是构建的目标文件也可以是外部脚本等等,后面连接传递给可执行文件的参数。

如果没有在同一个CMakeLists.txt中打开ENABLE_TESTING()指令,任何ADD_TEST都是无效的。比如前面的Helloworld例子,可以在工程主CMakeLists.txt中添加:

ADD_TEST(mytest ${PROJECT_BINARY_DIR}/bin/main)
ENABLE_TESTING

生成Makefile后,就可以运行make test来执行测试了。

AUX_SOURCE_DIRECTORY
基本语法是:AUX_SOURCE_DIRECTORY(dir VARIABLE),作用是发现一个目录下所有的源代码文件并将列表存储在一个变量中,这个指令临时被用来自动构建源文件列表,因为目前cmake还不能自动发现新添加的源文件。比如:

AUX_SOURCE_DIRECTORY(. SRC_LIST)
ADD_EXECUTABLE(main ${SRC_LIST})

可以通过后面提到的FOR EACH指令来处理这个LIST。

CMAKE_MINIMUM_REQUIRED(VERSION versionNumber [FATAL_ERROR])
比如:CMAKE_MINIMUM_REQUIRED(VERSION 2.5 FATAL_ERROR)
如果cmake版本小与2.5,则出现严重错误,整个过程中止。

EXEC_PROGRAM
在CMakeLists.txt处理过程中执行命令,并不会在生成的Makefile中执行。具体语法为:

EXEC_PROGRAM(Executable [directory in which to run] [ARGS <arguments to executable>] [OUTPUT_VARIABLE <var>] [RETURN_VALUE <var>])
用于在指定的目录运行某个程序,通过ARGS添加参数,如果要获取输出和返回值,可通过OUTPUT_VARIABLE和RETURN_VALUE分别定义两个变量。

这个指令可以帮助在CMakeLists.txt处理过程中支持任何命令,比如根据系统情况去修改代码文件等等。举个简单的例子,我们要在src目录执行ls命令,并把结果和返回值存下来,可以直接在src/CMakeLists.txt中添加:

EXEC_PROGRAM(ls ARGS "*.c" OUTPUT_VARIABLE LS_OUTPUT RETURN_VALUE LS_RVALUE)
IF(not LS_RVALUE)
    MESSAGE(STATUS "ls result: " ${LS_OUTPUT})
ENDIF(not LS_RVALUE)

在cmake生成Makefile过程中,就会执行ls命令,如果返回0,则说明成功执行,那么就输出ls *.c的结果。关于IF语句,后面的控制指令会提到。

FILE指令

文件操作指令,基本语法为:

FILE(WRITEfilename "message to write"... )
FILE(APPENDfilename "message to write"... )
FILE(READfilename variable)
FILE(GLOBvariable [RELATIVE path] [globbing expression_r_rs]...)
FILE(GLOB_RECURSEvariable [RELATIVE path] [globbing expression_r_rs]...)
FILE(REMOVE[directory]...)
FILE(REMOVE_RECURSE[directory]...)
FILE(MAKE_DIRECTORY[directory]...)
FILE(RELATIVE_PATHvariable directory file)
FILE(TO_CMAKE_PATHpath result)
FILE(TO_NATIVE_PATHpath result)

INCLUDE指令

用来载入CMakeLists.txt文件,也用于载入预定义的cmake模块。

INCLUDE(file1[OPTIONAL])
INCLUDE(module[OPTIONAL])

OPTIONAL参数的作用是文件不存在也不会产生错误,可以指定载入一个文件,如果定义的是一个模块,那么将在CMAKE_MODULE_PATH中搜索这个模块并载入,载入的内容将在处理到INCLUDE语句是直接执行。

FIND_指令

FIND_系列指令主要包含一下指令:

FIND_FILE(<VAR>name1 path1 path2 …)    VAR变量代表找到的文件全路径,包含文件名
FIND_LIBRARY(<VAR>name1 path1 path2 …)    VAR变量表示找到的库全路径,包含库文件名
FIND_PATH(<VAR>name1 path1 path2 …)   VAR变量代表包含这个文件的路径
FIND_PROGRAM(<VAR>name1 path1 path2 …)   VAR变量代表包含这个程序的全路径
FIND_PACKAGE(<name>[major.minor] [QUIET] [NO_MODULE] [[REQUIRED|COMPONENTS][componets...]])   用来调用预定义在CMAKE_MODULE_PATH下的Find<name>.cmake模块,也可以自己定义Find<name>模块,通过SET(CMAKE_MODULE_PATH dir)将其放入工程的某个目录中供工程使用,后面会详细介绍FIND_PACKAGE的使用方法和Find模块的编写。

FIND_LIBRARY示例:

FIND_LIBRARY(libXX11 /usr/lib)
IF(NOT libX)
    MESSAGE(FATAL_ERROR "libX not found")
ENDIF(NOT libX)

IF指令

IF指令,基本语法为:

IF(expression_r_r)
    #THEN section.
    COMMAND1(ARGS…)
    COMMAND2(ARGS…)
    …
ELSE(expression_r_r)
    #ELSE section.
    COMMAND1(ARGS…)
    COMMAND2(ARGS…)
    …
ENDIF(expression_r_r)

另外一个指令是ELSEIF,总体把握一个原则,凡是出现IF的地方一定要有对应的ENDIF,出现ELSEIF的地方,ENDIF是可选的。表达式的使用方法如下:

IF(var)  如果变量不是:空, 0, N, NO, OFF, FALSE, NOTFOUND 或 <var>_NOTFOUND时,表达式为真。
IF(NOT var), 与上述条件相反。
IF(var1AND var2), 当两个变量都为真是为真。
IF(var1OR var2), 当两个变量其中一个为真时为真。
IF(COMMANDcmd), 当给定的cmd确实是命令并可以调用是为真。
IF(EXISTS dir) or IF(EXISTS file), 当目录名或者文件名存在时为真。
IF(file1IS_NEWER_THAN file2), 当file1比file2新,或者file1/file2其中有一个不存在时为真文件名请使用完整路径。
IF(IS_DIRECTORY dirname),  当dirname是目录时为真。
IF(variableMATCHES regex)

IF(string MATCHES regex) 当给定的变量或者字符串能够匹配正则表达式regex时为真。比如:

IF("hello" MATCHES "hello")
    MESSAGE("true")
ENDIF("hello" MATCHES "hello")
IF(variable LESS number)
IF(string LESS number)
IF(variable GREATER number)
IF(string GREATER number)
IF(variable EQUAL number)
IF(string EQUAL number)

数字比较表达式

IF(variable STRLESS string)
IF(string STRLESS string)
IF(variable STRGREATER string)
IF(string STRGREATER string)
IF(variable STREQUAL string)
IF(string STREQUAL string)

按照字母序的排列进行比较。

IF(DEFINED variable),如果变量被定义,为真。
一个小例子,用来判断平台差异:

IF(WIN32)
    MESSAGE(STATUS“This is windows.”) #作一些Windows相关的操作
ELSE(WIN32)
    MESSAGE(STATUS“This is not windows”) #作一些非Windows相关的操作
ENDIF(WIN32)

上述代码用来控制在不同的平台进行不同的控制,但是阅读起来却并不是那么舒服, ELSE(WIN32)之类的语句很容易引起歧义。
这就用到了我们在 常用变量 一节提到的CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS开关。可以SET(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTSON),这时候就可以写成:

IF(WIN32)

ELSE()

ENDIF()

如果配合ELSEIF使用,可能的写法是这样:

IF(WIN32)
    #dosomething related to WIN32
ELSEIF(UNIX)
    #dosomething related to UNIX
ELSEIF(APPLE)
    #dosomething related to APPLE
ENDIF(WIN32)

WHILE指令

WHILE指令的语法是:

WHILE(condition)
    COMMAND1(ARGS…)
    COMMAND2(ARGS…)
    …
ENDWHILE(condition)

其真假判断条件可以参考IF指令。

FOREACH指令

FOREACH指令的使用方法有三种形式:
(1)列表

FOREACH(loop_vararg1 arg2 …)
    COMMAND1(ARGS…)
    COMMAND2(ARGS…)
    …
ENDFOREACH(loop_var)

像我们前面使用的AUX_SOURCE_DIRECTORY的例子

AUX_SOURCE_DIRECTORY(.SRC_LIST)
FOREACH(F ${SRC_LIST})
    MESSAGE(${F})
ENDFOREACH(F)

(2)范围

FOREACH(loop_var RANGE total)

ENDFOREACH(loop_var)

从0到total以1为步进,举例如下:

FOREACH(VARRANGE 10)
    MESSAGE(${VAR})
ENDFOREACH(VAR)

最终得到的输出是:

0
1
2
3
4
5
6
7
8
9
10

(3)范围和步进

FOREACH(loop_var RANGE start stop [step])

ENDFOREACH(loop_var)

从start开始到stop结束,以step为步进。举例如下:

FOREACH(A RANGE 5 15 3)
    MESSAGE(${A})
ENDFOREACH(A)

最终得到的结果是:

5
8
11
14

这个指令需要注意的是,直到遇到ENDFOREACH指令,整个语句块才会得到真正的执行。

复杂的例子:模块的使用和自定义模块

这里将着重介绍系统预定义的Find模块的使用以及自己编写Find模块,系统中提供了其他各种模块,一般情况需要使用INCLUDE指令显式的调用,FIND_PACKAGE指令是一个特例,可以直接调用预定义的模块。

其实纯粹依靠cmake本身提供的基本指令来管理工程是一件非常复杂的事情,所以cmake设计成了可扩展的架构,可以通过编写一些通用的模块来扩展cmake。

首先介绍一下cmake提供的FindCURL模块的使用,然后基于前面的libhello共享库,编写一个FindHello.cmake模块。

(一)使用FindCURL模块

在/backup/cmake目录建立t5目录,用于存放CURL的例子。
建立src目录,并建立src/main.c,内容如下:

#include<curl/curl.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

FILE*fp;

intwrite_data(void *ptr, size_t size, size_t nmemb, void *stream)

{
    int written = fwrite(ptr, size, nmemb, (FILE *)fp);
    return written;
}

int main()
{
    const char *path = “/tmp/curl-test”;
    const char *mode = “w”;
    fp = fopen(path,mode);
    curl_global_init(CURL_GLOBAL_ALL);
    CURL coderes;
    CURL *curl = curl_easy_init();

    curl_easy_setopt(curl, CURLOPT_URL, “http://www.linux-ren.org”);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
    curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
    res = curl_easy_perform(curl);
    curl_easy_cleanup(curl);
}

这段代码的作用是通过curl取回www.Linux-ren.org的首页并写入/tmp/curl-test文件中

建立主工程文件CmakeLists.txt,如下:

PROJECT(CURLTEST)
ADD_SUBDIRECTORY(src)

建立src/CmakeLists.txt

ADD_EXECUTABLE(curltest main.c)

现在自然是没办法编译的,我们需要添加curl的头文件路径和库文件。

方法1: 直接通过INCLUDE_DIRECTORIES和TARGET_LINK_LIBRARIES指令添加

我们可以直接在src/CMakeLists.txt中添加:

INCLUDE_DIRECTORIES(/usr/include)
TARGET_LINK_LIBRARIES(curltestcurl)

然后建立build目录进行外部构建即可。
现在要探讨的是使用cmake提供的FindCURL模块。

方法2: 使用FindCURL模块。向src/CMakeLists.txt中添加

FIND_PACKAGE(CURL)
IF(CURL_FOUND)
    INCLUDE_DIRECTORIES(${CURL_INCLUDE_DIR})
    TARGET_LINK_LIBRARIES(curltest${CURL_LIBRARY})
ELSE(CURL_FOUND)
    MESSAGE(FATAL_ERROR”CURL library not found”)
ENDIF(CURL_FOUND)

对于系统预定义的Find<name>.cmake模块,使用方法一般如上例所示,每一个模块都会定义以下几个变量:

<name>_FOUND
<name>_INCLUDE_DIR or <name>_INCLUDES
<name>_LIBRARY or <name>_LIBRARIES

可以通过<name>_FOUND来判断模块是否被找到,如果没有找到,按照工程的需要关闭某些特性、给出提醒或者中止编译,上面的例子就是报出致命错误并终止构建。
如果<name>_FOUND为真,则将<name>_INCLUDE_DIR加入INCLUDE_DIRECTORIES,将<name>_LIBRARY加入TARGET_LINK_LIBRARIES中。

我们再来看一个复杂的例子,通过<name>_FOUND来控制工程特性:

SET(mySourcesviewer.c)
SET(optionalSources)
SET(optionalLibs)

FIND_PACKAGE(JPEG)

IF(JPEG_FOUND)
    SET(optionalSources${optionalSources} jpegview.c)
    INCLUDE_DIRECTORIES(${JPEG_INCLUDE_DIR} )
    SET(optionalLibs${optionalLibs} ${JPEG_LIBRARIES} )
    ADD_DEFINITIONS(-DENABLE_JPEG_SUPPORT)
ENDIF(JPEG_FOUND)

IF(PNG_FOUND)
    SET(optionalSources${optionalSources} pngview.c)
    INCLUDE_DIRECTORIES(${PNG_INCLUDE_DIR} )
    SET(optionalLibs${optionalLibs} ${PNG_LIBRARIES} )
    ADD_DEFINITIONS(-DENABLE_PNG_SUPPORT)
ENDIF(PNG_FOUND)

ADD_EXECUTABLE(viewer${mySources} ${optionalSources}
TARGET_LINK_LIBRARIES(viewer${optionalLibs}

通过判断系统是否提供了JPEG库来决定程序是否支持JPEG功能。

(二)编写属于自己的FindHello模块

接下来在t6示例中演示如何自定义FindHELLO模块并使用这个模块构建工程。在/backup/cmake/中建立t6目录,并在其中建立cmake目录用于存放我们自己定义的FindHELLO.cmake模块,同时建立src目录,用于存放我们的源文件。

1.定义cmake/FindHELLO.cmake模块
FIND_PATH(HELLO_INCLUDE_DIR hello.h /usr/include/hello /usr/local/include/hello)

FIND_LIBRARY(HELLO_LIBRARY NAMES hello PATH /usr/lib /usr/local/lib)

IF(HELLO_INCLUDE_DIR AND HELLO_LIBRARY)
    SET(HELLO_FOUNDTRUE)
ENDIF(HELLO_INCLUDE_DIR AND HELLO_LIBRARY)

IF(HELLO_FOUND)
    IF(NOT HELLO_FIND_QUIETLY)
        MESSAGE(STATUS"Found Hello: ${HELLO_LIBRARY}")
    ENDIF(NOT HELLO_FIND_QUIETLY)
ELSE(HELLO_FOUND)
    IF(HELLO_FIND_REQUIRED)
        MESSAGE(FATAL_ERROR"Could not find hello library")
    ENDIF(HELLO_FIND_REQUIRED)
ENDIF(HELLO_FOUND)

针对上面的模块让我们再来回顾一下FIND_PACKAGE指令:

FIND_PACKAGE(<name>[major.minor] [QUIET] [NO_MODULE] [[REQUIRED|COMPONENTS][componets...]])

前面的CURL例子中我们使用了最简单的FIND_PACKAGE指令,其实它可以使用多种参数:

QUIET参数,对应与我们编写的FindHELLO中的HELLO_FIND_QUIETLY,如果不指定这个参数,就会执行:

MESSAGE(STATUS"Found Hello: ${HELLO_LIBRARY}")

REQUIRED参数,其含义是指这个共享库是否是工程必须的,如果使用了这个参数,说明这个链接库是必备库,如果找不到这个链接库,则工程不能编译。对应于FindHELLO.cmake模块中的HELLO_FIND_REQUIRED变量。
同样,我们在上面的模块中定义了HELLO_FOUND,HELLO_INCLUDE_DIR, HELLO_LIBRARY变量供开发者在FIND_PACKAGE指令中使用。

下面建立src/main.c,内容为:

#include<hello.h>
int main()
{
    HelloFunc();
    return 0;
}

建立src/CMakeLists.txt文件,内容如下:

FIND_PACKAGE(HELLO)
IF(HELLO_FOUND)
    ADD_EXECUTABLE(hellomain.c)
    INCLUDE_DIRECTORIES(${HELLO_INCLUDE_DIR})
    TARGET_LINK_LIBRARIES(hello${HELLO_LIBRARY})
ENDIF(HELLO_FOUND)

为了能够让工程找到 FindHELLO.cmake 模块(存放在工程中的cmake目录)我们在主工程文件 CMakeLists.txt 中加入:

SET(CMAKE_MODULE_PATH${PROJECT_SOURCE_DIR}/cmake)

(三)使用自定义的FindHELLO模块构建工程

仍然采用外部编译的方式,建立build目录,进入目录运行:

cmake ..
我们可以从输出中看到:

FoundHello: /usr/lib/libhello.so

如果我们把上面的FIND_PACKAGE(HELLO)修改为FIND_PACKAGE(HELLO QUIET),

不会看到上面的输出。接下来就可以使用make命令构建工程,运行:

./src/hello

可以得到输出

HelloWorld

说明工程成功构建。

(四)如果没有找到hellolibrary呢?

我们可以尝试将/usr/lib/libhello.x移动到/tmp目录,这样按照FindHELLO模块的定义,找不到hellolibrary了,我们再来看一下构建结果:

cmake ..

仍然可以成功进行构建,但是这时候是没有办法编译的。

修改FIND_PACKAGE(HELLO)为FIND_PACKAGE(HELLO REQUIRED),将hellolibrary定义为工程必须的共享库。
这时候再次运行

cmake ..

我们得到如下输出:

CMakeError: Could not find hello library.

因为找不到libhello.x,所以,整个Makefile生成过程被出错中止。

其它

1.怎样区分debug、release版本

建立debug/release两目录,分别在其中执行cmake -D CMAKE_BUILD_TYPE=Debug(或Release),需要编译不同版本时进入不同目录执行make即可:

Debug版会使用参数-g;
Release版使用-O3–DNDEBUG

另一种设置方法——例如DEBUG版设置编译参数DDEBUG

IF(DEBUG_mode)
    add_definitions(-DDEBUG)
ENDIF()

在执行cmake时增加参数即可,例如cmake -D DEBUG_mode=ON

2.怎样设置条件编译

例如debug版设置编译选项DEBUG,并且更改不应改变CMakelist.txt
使用option command,eg:

option(DEBUG_mode"ON for debug or OFF for release" ON)
IF(DEBUG_mode)
    add_definitions(-DDEBUG)
ENDIF()

使其生效的方法:首先cmake生成makefile,然后make edit_cache编辑编译选项;linux下会打开一个文本框,可以更改,改完后再make生成目标文件——emacs不支持make edit_cache;

局限:这种方法不能直接设置生成的makefile,而是必须使用命令在make前设置参数;对于debug、release版本,相当于需要两个目录,分别先cmake一次,然后分别makeedit_cache一次;

期望的效果:在执行cmake时直接通过参数指定一个开关项,生成相应的makefile。

[附]CMake常用指令

推荐阅读更多精彩内容

  • 注:首发地址 1. 前言 当在做 Android NDK 开发时,如果不熟悉用 CMake 来构建,读不懂 CMa...
    cfanr阅读 15,197评论 1 45
  • CMake学习 本篇分享一下有关CMake的一些学习心得以及相关使用。 本文目录如下: [1、CMake介绍] [...
    AlphaGL阅读 9,404评论 11 77
  • CMake学习 参考自《Cmake Practice --Cjacker》 基本语法规则 变量的引用 变量使用${...
    techping阅读 1,346评论 0 6
  • 1.安装 $sudo apt-get install cmake 2.示例:简单的文件目录 sample |—...
    荷包蛋酱阅读 25,499评论 0 15
  • 可以设置的变量 使用方式cmake -DCMAKE_INSTALL_PREFIX=yourpath 或在CMAKE...
    赵海洋阅读 950评论 0 3