CMake
(跨平台的Make)是一个开源的、跨平台的构建系统。在 Android 中,我们使用 CMake
来构建 C/C++ 代码。这篇文章就将介绍 Cmake
的使用。
单项目的构建
如上图所示,C++ 构建需要经过预处理、编译、汇编、链接等步骤。Cmake
就是用来管理这些步骤的执行,相关的配置都放在 CMakeLists.txt
文件中。一个最简单的 CMakeLists.txt
配置示例如下:
cmake
# 指定运行此 CMakeLists.txt 文件所需的 CMake 最低版本为 3.22.1。
cmake_minimum_required(VERSION 3.22.1)
# 定义当前项目的名称为 "hello-jni"。
# 同时 CMake 会创建如 ${PROJECT_NAME} 这样的变量供后续配置使用。
project("hello-jni")
# add_library 指令用于将指定的源文件编译成一个库文件。
# 第一个参数 "hello-jni" 是生成的库文件的名称
# 第二个参数 SHARED" 表明生成的库类型为共享库(动态链接库),静态库则使用 STATIC 关键字
# 第三个参数 "hello-jni.cpp" 是用于编译库文件的源文件,即把这个文件编译成共享库。
add_library(hello-jni SHARED hello-jni.cpp)
# target_link_libraries 指令用于将目标库(或可执行文件)与其他库进行链接。
target_link_libraries(hello-jni # "hello-jni" 是要进行链接操作的目标库。
android # android 是 Android NDK 提供的系统库
log) # log 也是 Android NDK 提供的系统库,用于在 Android 平台上进行日志输出
从示例可以看出,我们可以通过在 CMakeLists.txt
中配置参数,来实现C++构建的功能。
Android NDK 提供的系统库,可以在 原生 API 中找到。需要注意
libandroid
、liblog
和android
、log
是指向相同的库,lib
只是 Linux 中的通用前缀。
多文件编译
当源文件很多时,有三种方法来设置需要编译的源文件的位置。
- 方案一:一一列举
scss
add_library(echo
SHARED
audio_main.cpp
audio_player.cpp
audio_recorder.cpp
audio_effect.cpp
audio_common.cpp
debug_utils.cpp)
- 方案二:使用 set 设置变量
scss
set(SOURCES audio_main.cpp
audio_player.cpp
audio_recorder.cpp
audio_effect.cpp
audio_common.cpp
debug_utils.cpp)
add_library(mylib ${SOURCES})
- 方案三:设置目录
scss
file(GLOB SOURCES "src/*.cpp")
add_library(mylib ${SOURCES})
引用了其他库的头文件
如果你的库引用了其他库的头文件,比如#include "utils/native_debug.h"
,那么你需要使用 target_include_directories
命令来指定这些头文件的位置。代码示例如下:
cmake
add_library(hello-jni SHARED hello-jni.cpp)
# 指定 hello-jni 引用头文件的位置
target_include_directories(hello-jni PRIVATE ${CMAKE_SOURCE_DIR}/path/to/include)
- PRIVATE 和 PUBLIC
PRIVATE
表示这些头文件仅用于 hello-libs
库的编译,不会传递给链接 hello-libs
的其他目标。而 PUBLIC
关键字,意味着这些头文件搜索路径不仅对当前目标的编译有效,还会传递给所有链接该目标的其他目标。
- CMAKE_SOURCE_DIR
CMAKE_SOURCE_DIR
是预定义变量,代表最顶层的 CMakeLists.txt
文件所在的目录路径。而 ${}
语法则是获取该变量的值。除了这个预定义变量外,常用的预定义变量有:
objectivec
CMAKE_CURRENT_SOURCE_DIR 表示当前正在处理的 CMakeLists.txt 文件所在的目录路径
include_directories
也是设置头文件的位置,不同的是 include_directories
设置的是全局的;而 target_include_directories
需要指定库,比如这里是 hello-jni
导入共享库
如果要导入已经编译好的静态库或者动态库,需要在 add_library
使用 IMPORTED
。同时需要使用 set_target_properties
来设置导入库的文件位置。
bash
// 设置 distribution_DIR 变量的值
set(distribution_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../distribution)
# 声明要导入一个静态库,库的逻辑名称为 lib_gmath。
# 静态库在编译时会被完整地链接到可执行文件或其他库中。
add_library(lib_gmath STATIC IMPORTED)
# 设置导入的静态库 lib_gmath 的实际文件位置。
set_target_properties(lib_gmath PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/gmath/lib/${ANDROID_ABI}/libgmath.a)
ANDROID_ABI
变量的值由 Gradle 在运行 CMake 命令时自动传入。变量的值就是当前正在构建的 ABI 类型。我们可以根据不同的ANDROID_ABI
值进行不同的处理,例如添加不同的编译选项,链接不同的预编译的库等等。
查找库
scss
find_library(
third-party-lib
third-party
PATHS ${CMAKE_SOURCE_DIR}/src/main/cpp/third_party/lib
)
target_link_libraries(
native-lib
${third-party-lib}
)
find_library
命令是用来寻找库的,语法是 find_library(variable_to_store_path library_name 路径)
,它会在设置的路径(如果没有设置,则使用预定义的路径)下查找名为 library_name
的库,然后将找到的库的路径存储在 variable_to_store_path
变量中。
链接顺序
bash
target_link_libraries(hello-jni # "hello-jni" 是要进行链接操作的目标库。
android # android 是 Android NDK 提供的系统库
log) # log 也是 Android NDK 提供的系统库,用于在 Android 平台上进行日志输出
由于 hello-jni
依赖 android
和 log
库,因此 hello-jni
应该放在 android
和 log
库前面,而不是后面。注意在 target_link_libraries
方法中这个依赖的顺序不能错误。
Cmake日志
Cmake 的日志格式如下:
cmake
message([<mode>] "message text")
其中 <mode>
是可选参数,用于指定消息的类型,代码示例如下:
cmake
message("This is a status message")
# WARNING 用于输出警告信息,通常会在控制台以醒目的方式显示。
message(WARNING "This is a warning message")
# 用于输出致命错误信息,CMake 会在输出该消息后终止配置过程
message(FATAL_ERROR "Required file not found")
# 用于输出作者自定义的警告信息
message(AUTHOR_WARNING "This is an author warning")
# 用于标记某个功能或特性已被弃用。
message(DEPRECATION "This feature is deprecated and will be removed in future versions")
Cmake语法
在 Cmake 中,我们可以使用逻辑判断和控制语句来实现更复杂的构建控制功能。
条件表达式
- 比较运算符 :
LESS
(小于)、GREATER
(大于)、EQUAL
(等于)、LESS_EQUAL
(小于等于)、GREATER_EQUAL
(大于等于)。
cmake
if(${MY_VARIABLE} LESS 10)
message("MY_VARIABLE is less than 10")
endif()
- 逻辑运算符 :
AND
(逻辑与)、OR
(逻辑或)、NOT
(逻辑非)。
cmake
if(${MY_VARIABLE} GREATER 5 AND ${MY_VARIABLE} LESS 15)
message("MY_VARIABLE is between 5 and 15")
endif()
- 字符串判断 :
STREQUAL
(字符串相等)、STRLESS
(字符串按字典序小于)、STRGREATER
(字符串按字典序大于)。
cmake
if(${MY_STRING} STREQUAL "hello")
message("MY_STRING is equal to 'hello'")
endif()
- 变量存在性判断:可以直接使用变量名来判断变量是否存在且不为空。
cmake
if(MY_VARIABLE)
message("MY_VARIABLE is defined and not empty")
endif()
if
cmake
if(condition)
# 当 condition 为真时执行的命令
COMMAND1()
COMMAND2()
elseif(another_condition)
# 当 another_condition 为真时执行的命令
COMMAND3()
else()
# 当所有前面的条件都为假时执行的命令
COMMAND4()
endif()
while
cmake
while(condition)
# 循环体命令
COMMAND()
endwhile()
代码示例如下:
cmake
set(COUNT 0)
while(${COUNT} LESS 5)
message("COUNT is ${COUNT}")
math(EXPR COUNT "${COUNT} + 1")
endwhile()
foreach
cmake
foreach(VAR IN ITEMS item1 item2 ...)
# 循环体命令
COMMAND(${VAR})
endforeach()
代码示例如下:
cmake
foreach(FILE IN ITEMS file1.txt file2.txt file3.txt)
message("Processing file: ${FILE}")
endforeach()
编译选项
使用 CMake 除了可以控制编译的流程外,还可以设置编译相关的配置。常见的编译选项有
- target_compile_features 和 target_compile_options
scss
# target_compile_features 可以设置编译特性,这里设置为采用 C++14 标准编译
target_compile_features(your-lib PUBLIC cxx_std_14)
# target_compile_options 命令可以用来添加编译选项,
# 这里添加了一个 -Wall 选项,它会让编译器输出所有类型的警告信息
target_compile_options(your-lib PRIVATE -Wall)
add_compile_options 是为项目中的所有目标(可执行文件、库等)添加编译选项
- set
使用 set
来设置一些配置选项。
bash
set(CMAKE_VERBOSE_MAKEFILE on) # 输出编译和链接信息
# `CMAKE_C_FLAGS` 是 CMake 中用于存储 C 编译器编译选项的变量。
# 此语句将 `-Wall`、`-Werror` 和 `-Wno-unused-function` 这三个编译选项追加到原有
# 的 `CMAKE_C_FLAGS` 变量值后面
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Werror -Wno-unused-function")
多项目的构建
如下所示,大项目都会分模块处理。对于这种情况,子项目的 CMakeLists.txt
不需要特殊处理。而根目录的 CMakeLists.txt
只需要使用 add_subdirectory
引入子项目就可以了。
objectivec
project/
│ CMakeLists.txt
│
├───subproject1
│ │ CMakeLists.txt
│ │ other code files...
│
└───subproject2
│ CMakeLists.txt
│ other code files...
代码示例如下:
scss
// subproject1 的 CMakeLists.txt
add_library(sub1 sub1.cpp)
target_link_libraries(sub1 ...)
// subproject2 的 CMakeLists.txt
add_library(sub2 sub2.cpp)
target_link_libraries(sub2 ...)
// 根目录的 CMakeLists.txt
add_subdirectory(subproject1)
add_subdirectory(subproject2)
add_executable(MyProject main.cpp)
target_link_libraries(MyProject sub1 sub2)
自定义命令
自定义命令看 CMake 高级特性