在linux 下进行开发很多人选择编写makefile 文件进行项目环境搭建,而makefile 文件依赖关系复杂,工作量很大,搞的人头很大。采用自动化的项目构建工具cmake 可以将程序员从复杂的makefile 文件中解脱出来。cmake 根据内置的规则和语法来自动生成相关的makefile 文件进行编译,同时还支持静态库和动态库的构建,我把工作中用到的东东总结在此,方便忘记时随时查看,具体cmake的介绍和详细语法还是参考官方文档(http://www.cmake.org/),有一篇中文的cmake 实践 写的不错,可以google一下。
使用cmake 很简单,只需要执行cmake, make 两个命令即可,用我工作中的一个工程举例说明。
假设当前的项目代码在src 目录。 src 下有子目录:server, utility, lib, bin, build
server ----- 存放项目的主功能类文件
utility ----- 存放项目要用到相关库文件,便已成为库文件存放到子目录lib 中
lib ----- 存放utility 生成的库
bin ----- 存放association 生成的二进制文件
build ----- 编译目录,存放编译生成的中间文件
cmake 要求工程主目录和所有存放源代码子目录下都要编写CMakeLists.txt 文件,注意大小写(cm 大写,list中l 大写且落下s).
src/CMakeLists.txt 文件如下:
相关解释:
1. CMakeLists.txt 文件中不区分大小写
2. PROJECT(project_name) 定义工程名称
语法:project(projectname [cxx] [c] [java])
可以指定工程采用的语言,选项分别表示:C++, C, java, 如不指定默认支持所有语言
3. MESSAGE(STATUS, "Content") 打印相关消息
输出消息,供调试CMakeLists.txt 文件使用。
4. SET(CMAKE_BUILE_TYPE DEBUG) 设置编译类型debug 或者release。 debug 版会生成相关调试信息,可以使用GDB 进行
调试;release不会生成调试信息。当无法进行调试时查看此处是否设置为debug.
5. SET(CMAKE_C_FLAGS_DEBUG "-g -Wall") 设置编译器的类型
CMAKE_C_FLAGS_DEBUG ---- C 编译器
CMAKE_CXX_FLAGS_DEBUG ---- C++ 编译器
6. ADD_SUBDIRECTORY(utility) 添加要编译的子目录
为工程主目录下的存放源代码的子目录使用该命令,各子目录出现的顺序随意。
如上便是工程server_project 主目录src 下的CMakeLists.txt 文件,下一篇我们解释子目录utiltiy中的CMakeLists.txt 文件。
子目录utility 下的CMakeLists.txt 文件如下:
相关解释:
1. SET(SOURCE_FILES .....)
表示要编译的源文件,所有的源文件都要罗列到此处。set 设置变量,变量名SOURCE_FILES自定义。
2. INCLUDE_DIRECTORY(...)
include头文件时搜索的所有目录
变量PROJECT_SOURCE_DIR 表示工程所在的路径,系统默认的变量
3. LINK_DIRECTORIES(...)
库文件存放的目录,在程序连接库文件的时候要再这些目录下寻找对应的库文件
4. ADD_LIBRARY(...)
表示生成静态链接库libassociaiton.a,由${PROJECT_SOURCE_DIR}代表的文件生成。
语法:ADD_LIBRARY(libname [SHARED|STATIC]
SHARED 表示生成动态库, STATIC表示生成静态库。
5. TARGET_LINK_LIBRARY(association core)
表示库association 依赖core库文件
6. SET_TARGET_PROPERTIES
设置编译的库文件存放的目录,还可用于其他属性的设置。如不指定,
生成的执行文件在当前编译目录下的各子目录下的build目录下,好拗口!简单一点:
如指定在: ./src/lib 下
不指定在: ./src/build/utility/build 目录下
生成的中间文件在./src/build/utilty/build 目录下,不受该命令额影响
子目录server 下的CMakeLists.txt 文件:
相关解释:
1. ADD_EXECUTABLE() #指定要生成的执行文件的名称server
其他用法同utilty/CMakeLists.txt
2. SET_TARGET_PROPERTIES
设置生成的执行文件存放的路径,
注意:
执行文件server 依赖的子目录utility 子目录生成的静态库libutility.a,在指定的时候要写成:
TARGET_LINK_LIBRARIES(server utility)
而不能写成:
TARGET_LINK_LIBRARIES(server libutility.a)
否则编译总会提示找不到libutility库文件。
但使用第三方的库却要指定成具体的库名,如:libACE-6.0.0.so
这一点很诡异,暂时还没找到原因。
完成对应的CMakeLists.txt 文件编写后,便可以进行编译了。
编译:
进入 ./src/build
执行cmake ..
make
cmake 的使用很简单,更高级的应用好比版本信息,打包,安装等相关基本的应用后面会一一介绍,
复杂的语法使用要参考官方文档。
一直想用Android Studio的新方式Cmake来编译JNI 代码,之前也尝试过,奈何有两个难题挡住了我
1. 只能生成一个 so库,不能一次性生成多个 so库,之前的mk是可以有子模块的。
2. 每次生成的so所在的目录不是在 jniLibs下,虽然知道如果打包,会将它打包进去,但就是觉得看不见它,想提供给别人用,还要去某个目录找。
经过尝试,这两个问题都可以解决了。
demo下载地址: http://download.csdn.net/detail/b2259909/9766081
直接看CMakeLists.txt文件:
#指定需要CMAKE的最小版本
cmake_minimum_required(VERSION 3.4.1)
#C 的编译选项是 CMAKE_C_FLAGS
# 指定编译参数,可选
SET(CMAKE_CXX_FLAGS "-Wno-error=format-security -Wno-error=pointer-sign")
#设置生成的so动态库最后输出的路径
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
#设置头文件搜索路径(和此txt同个路径的头文件无需设置),可选
#INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}/common)
#指定用到的系统库或者NDK库或者第三方库的搜索路径,可选。
#LINK_DIRECTORIES(/usr/local/lib)
add_library( native-lib
SHARED
src/main/cpp/native-lib.cpp )
target_link_libraries( native-lib
log )
其中 各个设置都有说明。主要看这个:
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
它将会把生成的so库按照你在 build.gradle 指定的 abi分别放置在 jniLibs下
非常好,先解决了第二个问题了。
还是上面那个demo,重新建一个module。
cpp的目录结构:
直接看CMakeLists.txt文件:
#指定需要CMAKE的最小版本
cmake_minimum_required(VERSION 3.4.1)
#C 的编译选项是 CMAKE_C_FLAGS
# 指定编译参数,可选
SET(CMAKE_CXX_FLAGS "-Wno-error=format-security -Wno-error=pointer-sign")
#设置生成的so动态库最后输出的路径
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
#设置头文件搜索路径(和此txt同个路径的头文件无需设置),可选
#INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}/common)
#指定用到的系统库或者NDK库或者第三方库的搜索路径,可选。
#LINK_DIRECTORIES(/usr/local/lib)
#添加子目录,将会调用子目录中的CMakeLists.txt
ADD_SUBDIRECTORY(one)
ADD_SUBDIRECTORY(two)
不同的地方是改为添加子目录:
#添加子目录,将会调用子目录中的CMakeLists.txt
ADD_SUBDIRECTORY(one)
ADD_SUBDIRECTORY(two)
这样就会先去跑到子目录下的 one 和 two 的CmakeLists.txt,执行成功再返回。
此时子目录one下的CmakeLists.txt:
#继承上一层的CMakeLists.txt的变量,也可以在这里重新赋值
#C 的编译选项是 CMAKE_C_FLAGS
# 指定编译参数,可选
#SET(CMAKE_CXX_FLAGS "-Wno-error=format-security -Wno-error=pointer-sign")
#生成so动态库
ADD_LIBRARY(one-lib SHARED one.cpp)
target_link_libraries(one-lib log)
子目录two下的CmakeLists.txt:
#继承上一层的CMakeLists.txt的变量,也可以在这里重新赋值
#C 的编译选项是 CMAKE_C_FLAGS
# 指定编译参数,可选
#SET(CMAKE_CXX_FLAGS "-Wno-error=format-security -Wno-error=pointer-sign")
#生成so动态库
ADD_LIBRARY(two-lib SHARED two.cpp)
target_link_libraries(two-lib log)
最后生成了以下两个so文件,并自动按照abi分别放置在了 jniLibs下:
第一个问题也成功了。
最后,除了 设定abiFilters 必须在 build.gradle
主要是发现CmakeLists.txt里 其实可以指定很多东西:
1. so输出路径 CMAKE_LIBRARY_OUTPUT_DIRECTORY
2. .a 静态库输出路径 CMAKE_ARCHIVE_OUTPUT_DIRECTORY
2. 获取当前编译的abi , ANDROID_ABI
3. 编译选项:
CMAKE_C_FLAGS
CMAKE_CXX_FLAGS
CMAKE_CXX_FLAGS_DEBUG/CMAKE_CXX_FLAGS_RELEASE
4. 子目录编译: ADD_SUBDIRECTORY
5. #设置.c文件集合的变量
#当前cmakelists.txt所在目录的所有.c .cpp源文件
AUX_SOURCE_DIRECTORY(. SRC_LIST)
#增加其他目录的源文件到集合变量中
list(APPEND SRC_LIST
../common/1.c
../common/2.c
../common/3.c
../common/4.c
../common/5.c
../common/WriteLog.c
)
#生成so库,直接使用变量代表那些.c文件集合
add_library(mylib SHARED ${SRC_LIST})
6._执行自定义命令:
# copy头文件到 静态库相同文件夹下
add_custom_command(TARGET myjni
PRE_BUILD
COMMAND echo "executing a copy command"
COMMAND cp ${CMAKE_CURRENT_SOURCE_DIR}/myjni.h ${PROJECT_SOURCE_DIR}/../../../build/outputs/staticLib/myjni/${ANDROID_ABI}
COMMENT "PRE_BUILD, so This command will be executed before building target myjni"
)
最后,因为很多时候,JNI的参数还要转为C的方式,当我们在JAVA层写了native方法,android IDE自动提示红色,这时按下 ALT + ENTER 可以自动生成JNI下的方法声明,并且入参也帮我们转换好了。不过有时候这个插件不生效。
所以我写了一个JNI 的入参转为 C/C++的代码插件: JNI-Convert-Var,直接在 plugin 的仓库搜就有了。
最近尝试实现android studio的ALT + ENTER 可以自动生成JNI下的方法声明,结果发现好多IntelliJ IDEA的接口不熟悉。 只能先放弃了,以下是我的逻辑:
当鼠标点击在 Native声明方法上时:
1. 检查文件类型,如果为java就继续
2. 获取当前行的上下共三行字符串数据,使用正则表达式获取native声明的完整方法。
3. 检查当前模块目录下的jni或者cpp目录下的.c或者.cpp文件。
4. 如果没有文件,弹窗让用户创建一个C/C++文件,并追加转换后(如何转换会有一个专门的类)的Java2C方法在文件末尾. 在IDE打开此文件。
5. 如果JNI或者cpp目录有一个以上的C/C++文件, 弹窗让用户选择一个C/C++文件或者创建,之后打开文件追加转换后(如何转换会有一个专门的类)的Java2C方法在文件末尾. 。 在IDE打开此文件。
上面逻辑中:
主要有隐式定义和显式定义两种。
隐式定义的一个例子是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
跟其它指令稍有区别,目前可以认为它们是一致的。
(out-of-source build与in-source build相对,指是否在CMakeLists.txt所在目录进行编译。)
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
指令定义的项目名称。
使用$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和CMAKE_LIBRARY_PATH
CMAKE_MAJOR_VERSION
,CMAKE主版本号,比如2.4.6中的2CMAKE_MINOR_VERSION
,CMAKE次版本号,比如2.4.6中的4CMAKE_PATCH_VERSION
,CMAKE补丁等级,比如2.4.6中的6CMAKE_SYSTEM
,系统名称,比如Linux-2.6.22CMAKE_SYSTEM_NAME
,不包含版本的系统名,比如LinuxCMAKE_SYSTEM_VERSION
,系统版本,比如2.6.22CMAKE_SYSTEM_PROCESSOR
,处理器名称,比如i686UNIX
,在所有的类Unix平台为TRUE
,包括OSX和cygwinWIN32
,在所有的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()
添加。
这里引入更多的cmake指令,为了编写的方便,将按照cmakeman page 的顺序介绍各种指令,不再推荐使用的指令将不再介绍。
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
ADD_EXECUTABLE(helloDemo demo.cxx demo_b.cxx)
指定编译,好像也可以添加.o文件,将cxx编译成可执行文件
ADD_LIBRARY
ADD_LIBRARY(Hellohello.cxx) #将hello.cxx编译成静态库如libHello.a
ADD_SUBDIRECTORY
ADD_SUBDIRECTORY(Hello) #包含子目录
ADD_TEST
ENABLE_TESTING
ENABLE_TESTING
指令用来控制Makefile是否构建test目标,涉及工程所有目录。语法很简单,没有任何参数,ENABLE_TESTING()
一般放在工程的主CMakeLists.txt中。
ADD_TEST
指令的语法是: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
语法为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语句是直接执行。
INSTALL指令
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([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(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(condition)
COMMAND1(ARGS…)
COMMAND2(ARGS…)
…
ENDWHILE(condition)
其真假判断条件可以参考IF指令。
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
#include
#include
#include
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>_FOUND
<name>_INCLUDE_DIR or <name>_INCLUDES
<name>_LIBRARY or <name>_LIBRARIES
可以通过
来判断模块是否被找到,如果没有找到,按照工程的需要关闭某些特性、给出提醒或者中止编译,上面的例子就是报出致命错误并终止构建。
如果
为真,则将
加入INCLUDE_DIRECTORIES
,将
加入TARGET_LINK_LIBRARIES
中。
我们再来看一个复杂的例子,通过
来控制工程特性:
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([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
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_minimum_required(VERSION 2.8.2 FATAL_ERROR)
project("ProjName")
// 不推荐使用add_definitions来设置编译选项,因为其作用如同cmake -D
add_definitions(
-std=c++11 # Or -std=c++0x
-Wall
-Wfatal-errors
-DXXX #等于gcc -DXXX
# Other flags
)
// 一般通过下条语句设置编译选项
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -Wfatal-errors -fPIC")
//变量:CMAKE_PREFIX_PATH: Path used for searching by FIND_XXX(), with appropriate suffixes added。
set(CMAKE_PREFIX_PATH /path/path/path)
//引用环境变量
export FOO=/use/lib # 在bash中
$ENV{FOO} # 在CMakeLists.txt中
//头文件路径
include_directories(
include
relative/path
/absolute/path/
)
//链接库目录
link_directories(directory1 directory2 ...)
//链接函数库
target_link_libraries(a.out mylib ompl) //可以是cmake中的target,也可以是某个目录中的库文件,如 libompl.so,等同于 gcc -lompl
//源文件路径,在子目录中
add_subdirectory (
someDirectory
src/otherDirecotry
)
//寻找某个目录中的所有源文件,格式:aux_source_directory( )
aux_source_directory(src _srcFiles)
//生成库文件
add_library(mylib [SHARED|STATIC]
mylib.cpp
other.cpp
)
//生成可执行程序
add_executable(a.out
main.cpp
src/func.cpp
)
//设置变量
set(someVariable "some string")
//打印消息
message(STATUS "some status ${someVariable}")
//list操作
list(REMOVE_ITEM _srcFiles "src/f4.cpp") //从变量中去掉某一项
list(APPEND <element> [<element> ...]) //添加某一项到变量中
//检查编译器是否支持某一个编译选项
CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11)
CHECK_CXX_COMPILER_FLAG("-std=c++0x" COMPILER_SUPPORTS_CXX0X)
if(COMPILER_SUPPORTS_CXX11)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
elseif(COMPILER_SUPPORTS_CXX0X)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")
else()
message(STATUS "The compiler ${CMAKE_CXX_COMPILER} has no C++11 support. Please use a different C++ compiler.")
endif()
常用 find_package 找 boost 库和头文件:
# Put this in your CMakeLists.txt file (change any options from OFF to ON if you want):
set(Boost_USE_STATIC_LIBS OFF) # 不写这几个就是默认设置
set(Boost_USE_MULTITHREADED ON)
set(Boost_USE_STATIC_RUNTIME OFF)
find_package(Boost 1.59.0 COMPONENTS *boost libraries here* REQUIRED) // 如果只是头文件库,则不需要写 COMPONENTS, 直接 find_package(Boost 1.59.0 xxx REQUIRED)
if(Boost_FOUND)
include_directories(${Boost_INCLUDE_DIRS})
add_executable(progname file1.cxx file2.cxx)
target_link_libraries(progname ${Boost_LIBRARIES})
endif()
# Obviously you need to put the libraries you want where I put *boost libraries here*.
# For example, if you're using the filesystem and regex library you'd write:
find_package(Boost 1.59.0 COMPONENTS filesystem regex REQUIRED) # 一般包含头文件是 , 则 COMPONENTS 后面就写 xxx
cmake marco & function
set(var "ABC")
macro(Moo arg) # 定义macro
message("arg = ${arg}")
set(arg "abc")
message("# After change the value of arg.")
message("arg = ${arg}")
endmacro()
message("=== Call macro ===")
Moo(${var}) # 调用macro
function(Foo arg) # 定义函数
message("arg = ${arg}")
set(arg "abc")
message("# After change the value of arg.")
message("arg = ${arg}")
endfunction()
message("=== Call function ===")
Foo(${var}) # 调用函数
and the output is
=== Call macro ===
arg = ABC
# After change the value of arg.
arg = ABC
=== Call function ===
arg = ABC
# After change the value of arg.
arg = abc
#注意,marco的调用相当于c/c++的预编译器,只会进行字符串替换(这就是为啥 arg 没有被改变);而函数则可以进行变量操作
//常用变量
CMAKE_SOURCE_DIR ( 相当于工程根目录 )
this is the directory, from which cmake was started, i.e. the top level source directoryCMAKE_CURRENT_SOURCE_DIR
this is the directory where the currently processed CMakeLists.txt is located inPROJECT_SOURCE_DIR ( =CMAKE_SOURCE_DIR 相当于工程根目录 )
contains the full path to the root of your project source directory, i.e. to the nearest directory where CMakeLists.txt contains the PROJECT() commandCMAKE_PREFIX_PATH (用于找 Findxxx.cmake文件,找 库 和 头文件)
Path used for searching by FIND_XXX(), with appropriate suffixes added.CMAKE_INSTALL_PREFIX ( 安装目录 )
Install directory used by install.
If “make install” is invoked or INSTALL is built, this directory is prepended onto all install directories. This variable defaults to /usr/local on UNIX and c:/Program Files on Windows.
例如:cmake .. -DCMAKE_INSTALL_PREFIX=/my/paht/to/install
// cmake的交叉编译环境设置,创建文件toolchain.cmake,添加以下内容:
# this one is important
SET(CMAKE_SYSTEM_NAME Linux)
#this one not so much
SET(CMAKE_SYSTEM_VERSION 1)
# specify the cross compiler
SET(CMAKE_C_COMPILER /opt/arm/arm-linux-gnueabihf-gcc)
SET(CMAKE_CXX_COMPILER /opt/arm/arm-linux-gnueabihf-g++)
# where is the target environment
SET(CMAKE_FIND_ROOT_PATH /opt/arm/install)
# search for programs in the build host directories
SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# for libraries and headers in the target directories
SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
// If this file is named toolchain.cmake and is located in your home directory
// and you are building in the subdirectory build then you can do:
~/src$ cd build
~/src/build$ cmake -DCMAKE_TOOLCHAIN_FILE=~/toolchain.cmake ..
注意: 在交叉编译的时候,如果某些 FindXXX.cmake 模块中有类似 pkg_search_module
或者 pkg_check_modules
等命令,则会有点问题:
FindXXX.cmake modules, which rely on executing a binary tool like pkg-config may have problems, since the pkg-config of the target platform cannot be executed on the host. Tools like pkg-config should be used only optional in FindXXX.cmake files.
可以找到相应的模块的 FindXXX.cmake 替换其 pkg-config
如果不想让 pkg-config 被执行,可以试着:
SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM ONLY) // 即不让cmake的 find_program 去host环境中找可执行文件,所以 pkg_search_module 这种命令应该会失败。
如果 cmake cache了一些变量,需要重新运行cmake,只需要删除 CMakeCache.txt 文件即可
尊重原作,以下部分复制了该作者的部分文件内容,see link
set(MYSIMPLEPACKAGE_ROOT_DIR
"${MYSIMPLEPACKAGE_ROOT_DIR}"
CACHE
PATH
"Directory to search")
if(CMAKE_SIZEOF_VOID_P MATCHES "8")
set(_LIBSUFFIXES /lib64 /lib)
else()
set(_LIBSUFFIXES /lib)
endif()
find_library(MYSIMPLEPACKAGE_LIBRARY
NAMES
mysimplepackage
PATHS
"${MYSIMPLEPACKAGE_ROOT_DIR}"
PATH_SUFFIXES
"${_LIBSUFFIXES}")
# Might want to look close to the library first for the includes.
get_filename_component(_libdir "${MYSIMPLEPACKAGE_LIBRARY}" PATH)
find_path(MYSIMPLEPACKAGE_INCLUDE_DIR
NAMES
mysimplepackage.h
HINTS
"${_libdir}" # the library I based this on was sometimes bundled right next to its include
"${_libdir}/.."
PATHS
"${MYSIMPLEPACKAGE_ROOT_DIR}"
PATH_SUFFIXES
include/)
# There's a DLL to distribute on Windows - find where it is.
set(_deps_check)
if(WIN32)
find_file(MYSIMPLEPACKAGE_RUNTIME_LIBRARY
NAMES
mysimplepackage.dll
HINTS
"${_libdir}")
set(MYSIMPLEPACKAGE_RUNTIME_LIBRARIES
"${MYSIMPLEPACKAGE_RUNTIME_LIBRARY}")
get_filename_component(MYSIMPLEPACKAGE_RUNTIME_LIBRARY_DIRS
"${MYSIMPLEPACKAGE_RUNTIME_LIBRARY}"
PATH)
list(APPEND _deps_check MYSIMPLEPACKAGE_RUNTIME_LIBRARY)
else()
get_filename_component(MYSIMPLEPACKAGE_RUNTIME_LIBRARY_DIRS
"${MYSIMPLEPACKAGE_LIBRARY}"
PATH)
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(MySimplePackage
DEFAULT_MSG
MYSIMPLEPACKAGE_LIBRARY
MYSIMPLEPACKAGE_INCLUDE_DIR
${_deps_check})
if(MYSIMPLEPACKAGE_FOUND)
set(MYSIMPLEPACKAGE_LIBRARIES "${MYSIMPLEPACKAGE_LIBRARY}")
set(MYSIMPLEPACKAGE_INCLUDE_DIRS "${MYSIMPLEPACKAGE_INCLUDE_DIR}")
mark_as_advanced(MYSIMPLEPACKAGE_ROOT_DIR)
endif()
mark_as_advanced(MYSIMPLEPACKAGE_INCLUDE_DIR
MYSIMPLEPACKAGE_LIBRARY
MYSIMPLEPACKAGE_RUNTIME_LIBRARY)