这个文档主要记录一下Cmake 在使用过程中一些细节问题,方面一以后查阅问题
什么是CMake
- 在Android Studio2.2和以上版本,构建原生库的默认工具是CMake
- CMake是一个跨平台的构建工具,可以使用简洁的语句来描述所有平台的安装(编译过程)。能够输出各种各样的makefile或者project文件。CMake并不直接构建出最终的软件,而是产生其他工具脚本比如makefile,然后在依据这个工具的构建方式使用。
- CMake是一个比make更高级的编译配置工具,它可以根据不同的平台,不同的编译器,生成相应的makefile或者vcproj项目,从而达到跨平台的目的。Android Studio利用CMake生成的是ninja。ninja是一个小型的关注速度的构建系统。我们一般不需要关心ninja的脚本,只需要知道怎么配置CMake就行了
- CMake是一个跨平台的支持产出各种不同的构建建脚本的一个工具。
- CMake源文件包含命令、注释、空格和换行
- 已CMakeLists.txt命名或者以.cmake为扩展名
- 可以通过add_subdirectory()命令吧子目录的CMake源文件添加进来
- CMake源文件中所有有效的语句都是命令,可以是内置命令或者自定义的函数或宏命令
单行注释使用 # ,多行注释使用#[[]]
#单行注释
#[[
多行注释多行注释
]]
CMake中所有变量都是string类型。可以使用set()和unset()命令来生命或者移除一个变量
变量的引用使用${变量名}
变量名是大小写敏感的
想要看到message命令打印的信息,build工程然后在路径app\externalNativeBuild\cmake\debug\armeabi-v7a\cmake_build_output.txt.txt中就能看到
#声明变量
set(name 123)
#引用变量 message是打印命令
message("name = ${name}")
- 列表也是字符串,可以把列表看成一个特殊的变量,它有多个值
- 语法格式:set(列表名 值1 值2…)
- 引用:${列表名}
#声明列表
set(list_var 1 2 3 4 5 )
#或者
set(list_1 "1;2;3;4;5")
#打印
message("list_var=${list_var}")
只要通过Android Studio 创建一个C++工程会默认创建一个CmakeList.txt
cmake_minimum_required(VERSION 3.4.1)
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
native-lib.cpp)
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib})
上面是一个简单的CmakeList文件,如果第一次看见这个txt文件可能对上面一脸懵逼,下面一点点分析一下这个配置文件
指定cmake支持的最低版本
- 添加一个库文件,名称为 native-lib
- 指定STATIC,SHSRED,MODULE参数来指定库的类型,STATIC:静态库;SHARED:动态库;MODULE:在使用dyld的系统中有效,如果不支持dyld等同于SHARED
- EXCLUDE_FORM_ALL:表示该库不会被默认构建,可选项目
- sorece1 sorece2….sourceN用来指定库的源文件
add_library(<name> [STATIC|SHARED|MODULE]
[EXCLUDE_FROM_ALL] source1 source2...sourceN)
target_link_libraries(<name> lib1 lib2 lib3)
#如果出现互相依赖的静态库,CMake会允许依赖图中包含循环依赖比如:
add_library(A STATIC a.c)
add_library(B STATIC b.c)
target_link_libraries(A B)
target_link_libraries(B A)
add_executable(main main.c)
target_link_libraries(main A)
被链接的库放在依赖它的库的后面,即如果上面的命令中lib1依赖于lib2,lib2又依赖于lib3,则在前面的命令中,必须严格按照lib1 lib2 lib3的顺序排列否则会报错
find_library( log-lib log) log-lib是起的别名字,库真正的名字是log
我们先看下目录结构
├── cpp
│ ├── CMakeLists.txt
│ ├── native-lib.cpp
│ └── one
│ ├── one.cpp
│ └── one.h
我们重点关注一下one 文件夹下的内容,就一个定义一个求和的方法
# one.h
#ifndef CMAKEDEMO_ONE_H
#define CMAKEDEMO_ONE_H
int sum(int a, int b);
#endif //CMAKEDEMO_ONE_H
# one.c
#include "one.h"
int sum(int a, int b) {
return a + b;
}
native-lib.cpp 使用到了one.h 文件
#include <jni.h>
#include <string>
#include "one/one.h"
#include <android/log.h>
#define LOG_TAG "nate"
#define LOGV(...) __android_log_write(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , LOG_TAG, __VA_ARGS__)
extern "C" JNIEXPORT jstring JNICALL
Java_com_nate_cmakedemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
int result = sum(2, 3);
LOGE("result= %d", result);
return env->NewStringUTF(hello.c_str());
}
逻辑很简单,这里面就是尝试涌cmake编译one.so,这是个动态so 我们看一下完成CMakeLists文件
cmake_minimum_required(VERSION 3.4.1)
add_library(
one
SHARED
one/one.cpp)
add_library(
native-lib
SHARED
native-lib.cpp)
find_library(
log-lib
log)
target_link_libraries(
native-lib
one
${log-lib})
这个地方首先编译出one.so,因为native-lib.so 用到了sum方法,所以target_link_libraries 要配置依赖关 native-lib依赖one
还是上面例子,我们把one.so变成静态库,首先先修改CMakeLists.txt文件
cmake_minimum_required(VERSION 3.4.1)
add_library(
one
STATIC
one/one.cpp)
add_library(
native-lib
SHARED
native-lib.cpp)
find_library(
log-lib
log)
target_link_libraries(
native-lib
one
${log-lib})
编译运行,没有任何问题,但是怎么证明one编译成静态库呢,我们去build的目录看一下
还是以上面的例子来讲解吧,one目录下只有一个.c 文件,但是在实际的开发过程中一个目录下可定不止一个c文件,那么如何有多个c文件需要如何配置呢?
解法有很多种类
- 最笨的方法是把用到c文件都配置到CMakeLists文件中
- 通过 aux_source_directory,这个方法可以把一个目录下的文件都编译进来,但是如果该目录下还有子目录就可以了
- 通过file方式,file方式可以把一个目录下的所以文件都编译进来,包括子目录的文件这个需要特殊方式指定才行
- 通过ADD_SUBDIRECTORY 方式,这种方式是在子目录下配置自己的CMakeLists文件,子目录的编译由自己的CMakeLists文件配置,这种方式比较好,因为他有自己的编译文件不影响到其他人
我们在one目录下增加个c文件two.cpp文件,内容和one文件基本一致
# two.h
#ifndef CMAKEDEMO_TWO_H
#define CMAKEDEMO_TWO_H
int add(int a,int b);
#endif //CMAKEDEMO_TWO_H
# two.cpp
#include "two.h"
int add(int a, int b) {
return a + b;
}
现在需要在native-lib文件中调用add 方法
#include <jni.h>
#include <string>
#include "one/one.h"
#include "one/two.h"
#include <android/log.h>
#define LOG_TAG "nate"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , LOG_TAG, __VA_ARGS__)
extern "C" JNIEXPORT jstring JNICALL
Java_com_nate_cmakedemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
int result = sum(2, 3);
LOGE("result= %d", result);
result = add(2, 3);
LOGE("result= %d", result);
return env->NewStringUTF(hello.c_str());
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
add_library(
one
STATIC
one/one.cpp
one/two.cpp
)
add_library(
native-lib
SHARED
native-lib.cpp)
find_library(
log-lib
log)
target_link_libraries(
native-lib
one
${log-lib})
看见没有这种方式就是你用到哪个文件就在CMakeLists文件中配置,这种情况只适合c文件不多的情况
通过这种方式我们需要对CMakeLists文件进行大量修改
cmake_minimum_required(VERSION 3.4.1)
file(GLOB SOURCE ./one/*.c ./one/*.cpp )
add_library(
native-lib
SHARED
${SOURCE}
native-lib.cpp)
find_library(
log-lib
log)
target_link_libraries(
native-lib
${log-lib})
file(GLOB SOURCE ./one/.c ./one/.cpp ) 这行配置是把one目录下的所有.c和.cpp文件赋值给变量 SOURCE,然后在add_library配置${SOURCE},注意file 还可以编译指定目录下的子目录,这不过这里我们还没用到
aux_source_directory 查找在某个路径下的所有源文件。
aux_source_directory(< dir > < variable >)
cmake_minimum_required(VERSION 3.4.1)
aux_source_directory(./one SOURCE)
add_library(
native-lib
SHARED
${SOURCE}
native-lib.cpp)
find_library(
log-lib
log)
target_link_libraries(
native-lib
${log-lib})
和上面唯一的区别就是这一句aux_source_directory(./one SOURCE),什么意思呢? 就是把one目录下的所以文件赋值给变量SOURCE,注意如果one目下有子目录则子目录下的文件是编译不进度的
通过ADD_SUBDIRECTORY方式一般比较适合编译第三方库,比如说JSC哈哈
目录结构
├── cpp
│ ├── CMakeLists.txt
│ ├── native-lib.cpp
│ └── one
│ ├── CMakeLists.txt
│ ├── one.cpp
│ ├── one.h
│ ├── two.cpp
│ └── two.h
我们先看一下one目录下的CMakeLists.txt文件如何配置吧
cmake_minimum_required(VERSION 3.4.1)
file(GLOB SOURCE ./*.c ./*.cpp )
add_library(
one
SHARED
${SOURCE}
)
target_link_libraries(
one
)
这个地方是one 编译成动态链接库 我们看一下主的CMakeLists文件配置是什么样子的
cmake_minimum_required(VERSION 3.4.1)
ADD_SUBDIRECTORY(one)
add_library(
native-lib
SHARED
native-lib.cpp)
find_library(
log-lib
log)
target_link_libraries(
native-lib
one
${log-lib})
最关键的地方是ADD_SUBDIRECTORY(one),查询CMake 官方文档 可以知道这条命令的作用是为构建添加一个子路径。子路径中的 CMakeLists.txt 也会被执行。
这个问题也没什么难度,我之前的one.so其实就是在一个项目中编译的多个so,现在我们调整一下我们c文件代码
├── cpp
│ ├── CMakeLists.txt
│ ├── native-lib.cpp
│ ├── one
│ │ ├── CMakeLists.txt
│ │ ├── one.cpp
│ │ └── one.h
│ └── two
│ ├── CMakeLists.txt
│ ├── two.cpp
│ └── two.h
我们把之前的two.cpp文件拆分到另外一个目录下,然后把one文件和two文件中的文件分别编译成one.so 和two.so 在two文件夹下增加CMakeLists文件
cmake_minimum_required(VERSION 3.4.1)
file(GLOB SOURCE ./*.c ./*.cpp )
add_library(
two
SHARED
${SOURCE}
)
target_link_libraries(
two
)
回到主CMakeLists文件
cmake_minimum_required(VERSION 3.4.1)
ADD_SUBDIRECTORY(one)
ADD_SUBDIRECTORY(two)
add_library(
native-lib
SHARED
native-lib.cpp)
find_library(
log-lib
log)
target_link_libraries(
native-lib
one
two
${log-lib})
通过ADD_SUBDIRECTORY方式增加要编译的子目录,我们看一下编译后的产物
├── libnative-lib.so
├── libone.so
└── libtwo.so
这个地方首先我们需要一个第三方库,暂时我们用cJson这个开源库,我们首先需要获取这个cJson的so文件,我们先编一个cJson的静态库 我们先看一下这个目录的结构
├── cjson
│ ├── CMakeLists.txt
│ ├── cJSON.c
│ ├── cJSON.h
│ ├── cJSON_Utils.c
│ └── cJSON_Utils.h
然后看一下这个目录下的CMakeLists.txt 如何配置
# 静态文件输出目录
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/../jniLibs/${CMAKE_BUILD_TYPE}/${ANDROID_ABI})
# so文件输出目录
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/../jniLibs/${CMAKE_BUILD_TYPE}/${ANDROID_ABI})
file(GLOB SOURCE ./*.c ./*.cpp )
add_library(
cjson
STATIC
${SOURCE}
)
然后在主的CMakeLists文件添加子目录编译
cmake_minimum_required(VERSION 3.4.1)
ADD_SUBDIRECTORY(one)
ADD_SUBDIRECTORY(two)
ADD_SUBDIRECTORY(cjson)
add_library(
native-lib
SHARED
native-lib.cpp)
find_library(
log-lib
log)
target_link_libraries(
native-lib
one
two
cjson
${log-lib})
然后编译生成这个静态的.a文件
├── jniLibs
│ └── Debug
│ ├── arm64-v8a
│ │ └── libcjson.a
│ └── armeabi-v7a
│ └── libcjson.a
下面我们假设libcjson.a这个是我们从外部引入的第三方so,我们在项目中如何使用呢?
在主的CMakeLists文件中增加如下配置信息
ADD_SUBDIRECTORY(one)
ADD_SUBDIRECTORY(two)
#ADD_SUBDIRECTORY(cjson)
#引入头文件,主要cjson的头文件
include_directories(include)
#引入已经编译好的so
add_library(
cjson
SHARED
IMPORTED
)
# 这是固定的写法,用来描述已经编译好的so所以在的位置
set_target_properties( # Specifies the target library.
cjson
# Specifies the parameter you want to define.
PROPERTIES IMPORTED_LOCATION
# Provides the path to the library you want to import.
${PROJECT_SOURCE_DIR}/../jniLibs/Debug/${ANDROID_ABI}/libcjson.a )
target_link_libraries(
native-lib
one
# 这个地方是使用到的地方
cjson
two
${log-lib})
修改native-lib.cpp文件我们在这里增加获取cjson版本号码的能力
#include <jni.h>
#include <string>
#include "one/one.h"
#include "two/two.h"
#include <android/log.h>
#include "./include/cJSON.h"
#define LOG_TAG "nate"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , LOG_TAG, __VA_ARGS__)
extern "C" JNIEXPORT jstring JNICALL
Java_com_nate_cmakedemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
int result = sum(2, 3);
LOGE("result= %d", result);
result = add(2, 3);
LOGE("result= %d", result);
//获取版本号码
const char *version = cJSON_Version();
LOGE("cjson version= %s", version);
return env->NewStringUTF(hello.c_str());
}
日志输出:
2020-03-01 11:03:22.150 28271-28271/? E/nate: result= 5
2020-03-01 11:03:22.150 28271-28271/? E/nate: result= -1
2020-03-01 11:03:22.150 28271-28271/? E/nate: cjson version= 1.7.10
总体上配置信息还是比较麻烦,一般的时候我们也用不到这种方式,但是如果有的人就提供so不提供源文件,那只能用这种方式了
还有一种更简单的方式实现
可以通过link_directories,指定依赖的so位置来使用第三方so
该指令的作用主要是指定要链接的库文件的路径,该指令有时候不一定需要。因为find_package和find_library指令可以得到库文件的绝对路径。不过你自己写的动态库文件放在自己新建的目录下时,可以用该指令指定该目录的路径以便工程能够找到
例子:
link_directories(
lib
)
只需要在主的CMakeLists文件中增加这两个配置就可以了
#引入头文件,主要cjson的头文件
include_directories(include)
#5.对应的库
link_libraries(${PROJECT_SOURCE_DIR}/../jniLibs/Debug/${ANDROID_ABI}/libcjson.a)
和第一种方式对比,很显然这种方式很简单
第一种方式的配置很复杂
#引入已经编译好的so
add_library(
cjson
SHARED
IMPORTED
)
# 这是固定的写法,用来描述已经编译好的so所以在的位置
set_target_properties( # Specifies the target library.
cjson
# Specifies the parameter you want to define.
PROPERTIES IMPORTED_LOCATION
# Provides the path to the library you want to import.
${PROJECT_SOURCE_DIR}/../jniLibs/Debug/${ANDROID_ABI}/libcjson.a )
# 这个地方是使用到的地方
cjson
two
${log-lib})
cmake中预设了一堆变量 系统信息
- CMAKE_MAJOR_VERSION cmake主版本号,如2.8.6中的2
- CMAKE_MINOR_VERSION cmake次版本号,如2.8.6中的8
- CMAKE_PATCH_VERSION cmake补丁等级,如2.8.6中的6
- CMAKE_SYSTEM 系统名称,例如Linux-2.6.22
- CAMKE_SYSTEM_NAME 不包含版本的系统名,如Linux
- CMAKE_SYSTEM_VERSION 系统版本,如2.6.22
- CMAKE_SYSTEM_PROCESSOR 处理器名称,如i686
- UNIX 在所有的类UNIX平台为TRUE,包括OS X和cygwin
- WIN32 在所有的win32平台为TRUE,包括cygwin
开关选项
- BUILD_SHARED_LIBS 控制默认的库编译方式。如果未进行设置,使用ADD_LIBRARY时又没有指定库类型,默认编译生成的库都是静态库 (可在t3中稍加修改进行验证)
- CMAKE_C_FLAGS 设置C编译选项
- CMAKE_CXX_FLAGS 设置C++编译选项
其他变量
- PROJECT_SOURCE_DIR 工程的根目录
- PROJECT_BINARY_DIR 运行cmake命令的目录,通常是${PROJECT_SOURCE_DIR}/build
- CMAKE_INCLUDE_PATH 环境变量,非cmake变量
- CMAKE_LIBRARY_PATH 环境变量
- CMAKE_CURRENT_SOURCE_DIR 当前处理的CMakeLists.txt所在的路径
- CMAKE_CURRENT_BINARY_DIR target编译目录
- CMAKE_CURRENT_LIST_FILE 输出调用这个变量的CMakeLists.txt的完整路径
- CMAKE_CURRENT_LIST_LINE 输出这个变量所在的行
- CMAKE_MODULE_PATH 定义自己的cmake模块所在的路径
message 用来输出信息
MESSAGE([SEND_ERROR | STATUS | FATAL_ERROR] “message to display” …)
- 向终端输出用户定义的信息或变量的值
- SEND_ERROR, 产生错误,生成过程被跳过
- STATUS, 输出前缀为—的信息
- FATAL_ERROR, 立即终止所有cmake过程
message("PROJECT_SOURCE_DIR--> ${PROJECT_SOURCE_DIR}")
message("PROJECT_BINARY_DIR--> ${PROJECT_BINARY_DIR}")
message("CMAKE_CURRENT_LIST_FILE--> ${CMAKE_CURRENT_LIST_FILE}")
message("name --> ${NAME}")
输出:
PROJECT_SOURCE_DIR--> /CmakeDemo/app/src/main/cpp
PROJECT_BINARY_DIR--> /CmakeDemo/app/.cxx/cmake/debug/arm64-v8a
CMAKE_CURRENT_LIST_FILE--> /CmakeDemo/app/src/main/cpp/CMakeLists.txt
name --> nate
这个地方有个疑问,就是这个message 是在哪里呢?
message 输出的地方是在build_output.txt文件中
set 这个用来定义一个变量
# 变量名为 var,值为 hello
set(var hello)
当需要引用变量时,在变量名外面加上 ${} 符合来引用变量。
# 引用 var 变量
${var}
还可以通过 message 在命令行中输出打印内容。
set(var hello)
message(${var})
CMake 通过 string 来实现字符串的操作,这波操作有很多,包括将字符串全部大写、全部小写、求字符串长度、查找与替换等操作。
具体查看 官方文档。
set(var "this is string")
set(sub "this")
set(sub1 "that")
# 字符串的查找,结果保存在 result 变量中
string(FIND ${var} ${sub1} result )
# 找到了输出 0 ,否则为 -1
message(${result})
# 将字符串全部大写
string(TOUPPER ${var} result)
message(${result})
# 求字符串的长度
string(LENGTH ${var} num)
message(${num})
另外,通过空白或者分隔符号可以表示字符串序列。
set(foo this is a list) // 实际内容为字符串序列
message(${foo})
当字符串中需要用到空白或者分隔符时,再用双括号""表示为同一个字符串内容。
set(foo "this is a list") // 实际内容为一个字符串
message(${foo})
CMake 中通过 file 来实现文件操作,包括文件读写、下载文件、文件重命名等。
# 文件重命名
file(RENAME "test.txt" "new.txt")
# 文件下载
# 把文件 URL 设定为变量
set(var "http://img.zcool.cn/community/0117e2571b8b246ac72538120dd8a4.jpg")
# 使用 DOWNLOAD 下载
file(DOWNLOAD ${var} "/Users/glumes/CLionProjects/HelloCMake/image.jpg")
在文件的操作中,还有两个很重要的指令 GLOB 和 GLOB_RECURSE 。
# GLOB 的使用
file(GLOB ROOT_SOURCE *.cpp)
# GLOB_RECURSE 的使用
file(GLOB_RECURSE CORE_SOURCE ./detail/*.cpp)
其中,GLOB 指令会将所有匹配 *.cpp 表达式的文件组成一个列表,并保存在 ROOT_SOURCE 变量中。
而 GLOB_RECURSE 指令和 GLOB 类似,但是它会遍历匹配目录的所有文件以及子目录下面的文件。
使用 GLOB 和 GLOB_RECURSE
- 有好处:就是当添加需要编译的文件时,不用再一个一个手动添加了,同一目录下的内容都被包含在对应变量中了,
- 但也有弊端,就是新建了文件,但是 CMake 并没有改,导致在编译时也会重新产生构建文件,要解决这个问题,就是动一动 CMake,让编译器检测到它有改变就好了。