概念
你需要知道的资料
本文可以帮助你完成对Cmake的简单了解以及快速上手,更多功能以及更现代的用法请参见以下链接:
Introduction · Modern CMake (modern-cmake-cn.github.io)
GitHub - ttroy50/cmake-examples: Useful CMake Examples
CMake Tutorial — Mastering CMake
CMake Reference Documentation — CMake 3.30.4 Documentation
为什么是CMake
用途
make 存在以下问题,因此更高级的跨平台的编译配置工具CMake应运而生
-
make 不通用于 Windows(仅在Unix 类系统通用)。
-
make 要准确指明项目之间的依赖关系,在有头文件时难以处理。
-
语法过于简单,难以做复杂判断(不如shell/python)。
-
不同的编译器g++ MSVC clang有不同的 flag 规则,参数可能不通用。
CMake 只需要写一份 CMakeLists.txt,就能统一的描述所有平台的编译过程,不再需要为交叉编译多写makefile。够在调用时生成当前系统所支持的构建系统。
- 跨平台
- 简单的,统一的语句
跨平台
产生其他工具的脚本,再依据这个工具的构建方式使用
根据不同的平台、不同的编译器,产出各种不同的构建脚本
- 各种各样的makefile或者project文件。
- Android Studio利用CMake生成的是ninja。一个小型的关注速度的构建系统。我们不需要关心ninja的脚本,知道怎么配置CMake就可以了
本质是封装makefile 或 vcproj 或 ninja
整个编译链包括:
-
cmake,用于跨平台以及简化底层编译脚本的工具。
-
cmake生成更底层的编译命令
makefile(或build.ninja) 这一过程一般不直接参与
-
比如 make解析.makefile文件进行命令执行,
-
比如 ninja 解析 .ninja文件进行命令执行(编译速度比makefile更快)。
-
-
C/C++语言的编译器(clang/gcc g++/cl/MSVC等等)。
简单的option
CMake 可以自动检测当前的编译器,需要添加哪些 flag。
比如 OpenMP,只需要在 CMakeLists.txt 中指明 target_link_libraries(a.out OpenMP::OpenMP_CXX) 即可。
CMake options:cmake运行时可以加入的命令行参数
-
比如 -D来定义对应的变量控制对应的cmake行为,甚至于前面的Build type我们完全可以不写(当然这是CLion,这个空必须得被填充),
-
-DCMAKE_BUILD_TYPE=Release,这个变量可以决定最终cmake生成的执行脚本是按照release的标准去运行的,
-
-DBUILD_SHARED_LIBS=ON,那么最终是会生成动态库而不是静态库。
广受欢迎
在Android Studio 2.2及以上,构建原生库的默认工具是CMake。Clion也使用CMake构建项目。
Lion如何编译项目生成可执行文件:
-
通过cmake配置选项运行整个项目的CMakeList.txt
-
生成makefile或其他底层脚本
-
通过对应的工具去执行这个脚本
-
运行编译好的程序
以Android Studio为例
之前使用
Android.mk
使用eclipse
构建新的库。
-
app/libs
下放置fmod.jar
Activity销毁后,jar包会做回收工作
fmodstudioapi11009android\api\lowlevel\lib\fmod.jar
-
app/src/main/cpp
放置inc
fmodstudioapi11009android\api\lowlevel\inc
-
创建
app/src/main/jniLibs
放置arm64-v8a
armeabi-v7a
armeabi
x86
fmodstudioapi11009android\api\lowlevel\lib
之下的arm64-v8a
(最新,指令集效率更高)armeabi-v7a
armeabi
(最老,被其他兼容)x86
MainActivity.java
import com.derry.as_jni_2_02.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
// 最终目标让 libfmod_learning01.so 动态库 libfmod_learning01.a 静态库
static {
// 加载 apk/lib/平台/lib项目名.so 库 (此库里面有 项目名.cpp 等代码)
// 需要使用Fmod,就需要让 lib项目名.so 包含Fmod, 通过CMake集成
System.loadLibrary("fmod_learning01"); // System.loadLibrary("项目名");
}
so
:linux Android的动态链接库
包 package
包 (package) 库的集合
-
现代 CMake 认为一个 包 (package) 可以提供多个 库,库又称组件 (components)。比如 TBB 这个包,就包含了 tbb, tbbmalloc, tbbmalloc_proxy 这三个组件。
-
原因
-
为避免冲突,每个包都享有一个独立的名字空间,以 :: 的分割(和 C++ 还挺像的)。
-
可以指定要使用包中的哪几个库(组件)
-
库
-
库中的函数可以被可执行文件调用,也可以被其他库文件调用。
-
若多个可执行文件,他们之间用到的某些相同的功能。就可以把共用的功能做成一个库,进行共享。
-
根据平台&种类 进行分类
-
dll
: windows的动态链接库 -
dawenlib
: Mac -
so
:linux Android的动态链接库
-
常用模块结构
复杂的工程中,我们需要划分子模块。通常一个库一个目录(一个包package由若干个 库 即 components组件 组成):
- 根目录
CMakeLists.txt
- 包含子目录的模块
- 子模块
根目录
CMakeLists.txt
- 用 CMake 的 add_subdirectory 添加子目录
- 可以使用子目录中定义的库
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
# 添加子目录下的模块
add_subdirectory(hellolib)
# 生成可执行文件
add_executable(a.out main.cpp)
# (编译产物中)使用子目录下的模块,链接模块了哪个库
target_link_libraries(a.out PUBLIC hellolib)
# 指定头文件目录
target_include_libraries(a.out PUBLIC hellolib)
源码文件
#include <项目名/模块名.h>
项目名::函数名()
子目录
包含一个 CMakeLists.txt, 定义了 子模块的生成规则
目录组织格式
项目名/include/项目名/模块名.h
项目名/src/模块名.cpp
命令行指令
指令即需要在终端编译整个工程的指令。
Install a Project
cmake --install <dir> [<options>]
Open a Project
cmake --open <dir>
Run a Command-Line Tool
cmake -E <command> [<options>]
Run the Find-Package Tool
cmake --find-package [<options>]
View Help
cmake --help[-<topic>]
build
编译
构建&编译
构建编译系统 Generate a Project Buildsystem
cmake [<options>] <path-to-source>
cmake [<options>] <path-to-existing-build>
cmake [<options>] -S <path-to-source> -B <path-to-build>
- -S source path
- -B build path
生成Makefile
# 读取当前目录的 CMakeLists.txt,并在 build 文件夹下生成 build/Makefile
cmake -B build # 由 CMakeLists.txt 生成 Makefile
根据Makefile执行编译
# 让 make 读取 build/Makefile,并开始构建 a.out:
make -C build # 由于Makefile构建 a.out
直接编译
Build a Project
cmake --build <dir> [<options>] [-- <build-tool-options>]
直接编译——例如直接生成可执行文件
# 生成 a.out
# # 更跨平台,更好的做法
cmake --build build # 默认生成a.out
cmake --build build --target a.out # 指定输出的目标
执行
# 执行生成的 a.out:
build/a.out
传参 -D
Run a Script
cmake [{-D <var>=<value>}...] -P <cmake-script-file>
-D变量名=xx 通过命令行给cmake设置参数。可以配合shell命令行传参
# 变量BUILD_PLATFROM对应编译平台
cmake -DBUILD_PLATFROM="aarch64p"
# 支持命令行传参
cmake -DBUILD_PLATFROM="$1"
# 执行编译
cmake --build build --target a.out
cmake_minimum_required(VERSION 3.0)
project(cpptestProject LANGUAGES CXX)
set(CMAKE_C_FLAGS "-Wall -DWITH_OPENSSL -DWITH_DOM")
#add_compile_options(-g)
# 设置平台
# - 默认值为X86
# - 可通过 cmake -D BUILD_PLATFROM="aarch64" 命令行赋值为aarch64 表示为arm平台
set(BUILD_PLATFROM "x86" CACHE STRING "default")
if(BUILD_PLATFROM STREQUAL "x86")
message("构建x86平台")
# # set(CMAKE_C_COMPILER "/usr/bin/gcc")
elseif(BUILD_PLATFROM STREQUAL "aarch64")
message("构建aarch64平台")
# # set(CMAKE_C_COMPILER "/usr/bin/aarch64-linux-gnu-gcc")
else()
message("构建其他平台")
endif()
封装一下
根据入参的文件(默认 main.cpp),生成可执行文件cpptest。
sh run yourOwn.cpp
run.sh
set -e
# 清除旧编译产物
rm -rf ./build
mkdir build
# 根据入参生成设置CMake变量 SRCFILE,并生成Makefile
cmake -B build -DSRCFILE=$1
# 开始构建
make -C build
# 执行生成的可执行文件
build/cpptest
对应的 CMakeList.txt
cmake_minimum_required(VERSION 3.16)
set(CMAKE_CXX_STANDARD 20)
project(hellocpp LANGUAGES CXX)
set(SRCFILE "main.cpp" CACHE STRING "default")
message("now build--->${SRCFILE}")
add_executable(cpptest ${SRCFILE})
target_link_libraries(cpptest PUBLIC pthread)
cmake 变量
预定义变量
CMake中所有变量都是string类型。
预定义变量有很多,比如:项目名称,输出目录(二进制文件目录),源文件目录,语言标准,编译器,环境变量,当前编译环境(操作系统,处理器)。
cmake-variables(7) — CMake 3.26.0-rc4 Documentation
用途 | 变量名 |
---|---|
项目名称 | PROJECT_NAME |
路径相关 | 变量名 |
---|---|
当前CmakeList 的路径 | CMAKE_SOURCE_DIR |
自动获取当前平台(4大平台) | CMAKE_ANDROID_ARCH_ABI |
输出目录(可执行文件和库文件)即二进制文件目录 | 变量名 |
---|---|
项目 | PROJECT_BINARY_DIR |
当前CMake运行 (通常相同) | CMAKE_BINARY_DIR |
源文件目录 包含CMakeLists.txt文件的目录 | 变量名 |
---|---|
项目 | PROJECT_SOURCE_DIR |
当前CMake运行 (通常相同) | CMAKE_SOURCE_DIR |
语言标准 | 变量 |
---|---|
C语言的标准版本 | CMAKE_C_STANDARD |
C++语言的标准版本 | CMAKE_CXX_STANDARD |
编译器 | 变量 |
---|---|
C编译器 | CMAKE_C_COMPILER_ID |
C++编译器 | CMAKE_CXX_COMPILER_ID |
环境变量 | 变量 |
---|---|
C | CMAKE_C_FLAGS |
C++ | CMAKE_CXX_FLAGS |
当前编译环境 | 变量 |
---|---|
操作系统名称(如Windows、Linux等) | CMAKE_SYSTEM_NAME |
处理器的类型(如x86、x86_64等) | CMAKE_SYSTEM_PROCESSOR |
链接可执行文件时使用的链接选项 | CMAKE_EXE_LINKER_FLAGS |
操作变量
定义 set
# 定义 set(变量名 变量值)
# 设置方式举例
set(CMAKE_CXX_STANDARD 17)
set(var 666)
移除 unset
# 移除 unset(变量名)
unset(var)
拼接 -L
-L
进行拼接的符号,可以用来进行变量的追加。
# 例如:将路径添加到环境变量
# - C++变量 CMAKE_CXX_FLAGS
# - C变量 CMAKE_C_FLAGS
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L ${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")
引用 $
# 引用变量 ${变量名}
message("var = ${var}") # 引用并打印
# 引用列表 ${列表名}
message("list_var = ${list_var}") # 引用并打印
列表
定义列表
# 定义列表
# # 方法1 set(列表名 值1 值2 ... 值N)
set(list_var 1 2 3 4 5)
# # 方法2 set(列表名 "值1;值2;...;值N")
set(list_var "1;2;3;4;5")
list
命令:APPEND
将元素追加到列表中
list(APPEND MyList Element)
# 例如:
# # EXTRA_LIBS 存储 MathFunctions 库
list(APPEND EXTRA_LIBS MathFunctions)
# # EXTRA_INCLUDES 存储 MathFunctions 头文件
list(APPEND EXTRA_INCLUDES ${PROJECT_SOURCE_DIR}/MathFunctions)
变量EXTRA_LIBS
用来保存需要链接到可执行程序的可选库
变量EXTRA_INCLUDES
用来保存可选的头文件搜索路径
cmake 命令
指定编译目标,链接,导入头文件子模块,打印log … 都是使用的cmake命令
这里堆放一些相对常用的,可以回过头来再看。因为最重要的命令我们很快就会逐个介绍。
-
project:用于定义项目名称、版本号和语言。
-
add_executable:用于添加可执行文件。第一个参数很重要,被称为target,可以作为target_xxx命令的接收对象。
-
add_library:用于添加库文件,可以创建静态库或动态库。第一个参数很重要,被称为target,可以作为target_xxx命令的接收对象。简单使用如下
-
add_definitions:用于添加宏定义,注意该命令没有执行顺序的问题,只要改项目中用了该命令定义宏,那么所有的源代码都会被定义这个宏
add_definitions(-DFOO -DBAR ...)
。 -
add_subdirectory:用于添加子项目目录,如果有该条语句,就先会跑去执行子项目的cmake代码,这样会导致一些需要执行后立马生效的语句作用不到,比如include_directories和link_directories如果执行在这条语句后面,则他们添加的目录在子项目中无法生效。有些命令如target_include_directories和target_link_directories是根据目标target是否被链接使用来生效的,所以这些命令的作用范围与执行顺序无关,且恰好同一个cmake项目中产生的库文件是可以直接通过名称链接的,无论链接对象是在子目录还是父目录
-
target_link_libraries:用于将可执行文件或库文件链接到库文件或可执行文件。身为target_xxx的一员,很明显第二个参数也可以进行权限控制。
-
include_directories:用于指定头文件搜索路径,优点是简单直接,缺点是无法进行权限控制,一旦被执行后,后续的所有代码都能搜索到对应的文件路径。
-
target_include_directories:指定头文件搜索路径,并将搜索路径关联到一个target上,这里的target一般是指生成可执行程序命令里的target或者生成库文件的target,与上一个命令的不同点在于可以设置导出权限,比如现在我写了一个项目,这个项目引入了其他库,但是我不想让其他库的符号暴露出去(毕竟使用这个项目的人只关注这个项目的接口,不需要关注其他依赖的接口)可以通过PRIVATE将头文件搜索目录设置不导出的权限。
-
link_directories:与前面的include_directories命令类似,添加的是库的搜索路径。
-
target_link_directories:和前面的include版本一样的,只是改成了库路径。
-
if\elseif\endif ,在编程语言立马已经用烂了,现在主要是了解 if(condition) 中的条件到底如何判断的,以及内部都支持哪些操作,比如大于等于啥的,这方面直接看官方文档吧,非常好懂:cmake.org/cmake/help/…
-
aux_source_directory:这个指令简单实用,第一个参数传递一个文件目录,它会扫描这里面所有的源文件放到第二个参数定义的变量名中。注意第一个参数只能是文件夹。
aux_source_directory(${PROJECT_SOURCE_DIR} SRC)
-
file:可以说是上面那个命令的增强版本,但如果熟悉这个命令的朋友肯定很快站出来反对,因为这个命令实在是太强大了,你如果翻一翻这个官方文档就会发现它具备几乎文件系统的所有功能,什么读写文件啊,什么从网上下载文件,本地上传文件之类的它都有,计算文件的相对路径,路径转化等等。但我们平时用到的最多的命令还是用来获取文件到变量里。比如file(GLOB FILES “文件路径表示1” “文件路径表示2” …) GLOB会产生一个由所有匹配globbing表达式的文件组成的列表,并将其保存到第二个参数定义的变量中。Globbing 表达式与正则表达式类似,但更简单,比如如果要实现前一个命令的功能可以这么写:
file(GLOB SRC "${PROJECT_SOURCE_DIR}/*.cc")
如果GLOB 换成GLOB_RECURSE ,那么上述命令将递归的搜寻其子目录的所有符合条件的文件,而不仅仅是一个层级。
-
execute_process:用于执行外部的命令,如下的示例代码是执行git clone命令,执行命令的工作目录在
${CMAKE_BINARY_DIR}/deps/
execute_process(COMMAND git clone https://github.com/<username>/<repository>.git WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/deps/<repository>) 复制代码
-
message:打印出信息用于debug。
-
option:用于快速设置定义变量并赋值为对应的bool值,常被用于判断某些操作是否执行。
-
find_package:用于查找外界的package,其实就是查找外界对应的
<package>Config.cmake
和Find<package>.cmake
文件,这些文件里有外界包对应的变量信息以及库和头文件的各种路径信息。我们需要注意一些有关find_package
命令查找 Config.cmake 路径的变量:CMAKE_PREFIX_PATH
变量是一个路径列表,CMake 会在这些路径中搜索包的Config.cmake
文件。<Package>_DIR
变量是指向包的Config.cmake
文件的路径。如果你手动设置了这个变量,那么find_package
命令就可以找到包的信息。
同时他的一些常用参数如下:
CONFIG
:显式指定find_package去查找<package>Config.cmake
文件,一般只要你在变量里面指定了<package>Config.cmake
的路径,那么该参数填不填都没差别。我建议最好还是带上该参数比较好。REQUIRED
:该参数表示如果没找到,那么直接产生cmake错误,退出cmake执行过程,如果没有REQUIRED,则即使没找到也不会终止编译。PATHS
:这个参数的效果和前面的变量类似,也是指定查找的路径。COMPONENTS
:用于指定查找的模块,模块分离在不同的文件中,需要使用哪个就指定哪个模块。典型的就是使用Qt时的cmake代码,比如find_package(Qt5 COMPONENT Core Gui Widgets REQUIRED)
。- VERSION:可能有很多个不同版本的包,则需要通过该参数来指定,如:
find_package(XXX VERSION 1.2.3)
。
-
include:从文件或模块加载并运行 CMake 代码。我用这个命令实际上只是为了使用 FetchContent¶ 这个module的功能,该功能是从cmake3.11开始支持的,使用该module前需要通过include命令加载该模块,命令如下:
include(FetchContent)
-
FetchContent:这是一个模块功能,它用来从代码仓库中拉取代码,例如我要把最近写的日志库引入到当前的项目中使用(注意这中间不会有任何代理,所以拉取GitHub的仓库可能失败):
include(FetchContent)#引入功能模块 FetchContent_Declare( my-logger #项目名称 GIT_REPOSITORY https://github.com/ACking-you/my-logger.git #仓库地址 GIT_TAG v1.6.2 #仓库的版本tag GIT_SHALLOW TRUE #是否只拉取最新的记录 ) FetchContent_MakeAvailable(my-logger) add_excutable(main ${SRC}) #链接到程序进行使用 target_link_libraries(main my-logger) 复制代码
这样引入第三方库的好处显而易见,优点类似于包管理的效果了,但缺少了最关键的中心仓库来确保资源的有效和稳定。参考golang再做个proxy层级就好了。 同样可以拉取最新的googletest可以使用下列语句:
FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG release-1.12.1 GIT_SHALLOW TRUE ) # For Windows: Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googletest) target_link_libraries(main gtest_main) 复制代码
-
function/endfunction :在cmake中用于定义函数,复用cmake代码的命令。第一个参数为函数的名称,后面为参数的名称,使用参数和使用变量时一样的,但是如果参数是列表类型,则在传入的时候就会被展开,然后与函数参数依次对应,多余的参数被
ARGN
参数吸收。
更多较为常用的命令:
- add_custom_command:添加自定义规则命令,同样也是执行外界命令,但多了根据依赖和产物判断执行时机的作用。
- install:添加install操作。
- string:对string的所有操作,比如字符串替换啥的。
- list:对list的所有操作,比如列表处理之类的。
- foreach:cmake中的for循环。
- …
利用上述命令实现Qt开发中调用uic工具把 大量的 .ui
文件转化为 .cpp
和 .h
文件,并实现当ui文件更新时或 .cpp/.h
文件不存在时才创建对应的 .cpp/.h
文件。
#函数功能实现
function(get_ui_source)
foreach (item ${ARGN})
set(UIC_EXE_PATH ${VCPKG_ROOT}/installed/x64-windows/tools/qt5/bin/uic.exe)
get_filename_component(name ${item} NAME_WLE)
string(PREPEND name "ui_")
set(output_h ${PROJECT_SOURCE_DIR}/ui_gen/${name}.h)
set(output_cpp ${PROJECT_SOURCE_DIR}/ui_gen/${name}.cpp)
file(TIMESTAMP ${item} ui_time)
#当.h文件已经存在时,仅当.ui文件被更新了才重新生成.h文件
if (EXISTS ${output_h})
file(TIMESTAMP ${output_h} h_time)
if (ui_time GREATER h_time)
execute_process(COMMAND ${UIC_EXE_PATH} ${item} -o ${output_h})
endif ()
else ()
execute_process(COMMAND ${UIC_EXE_PATH} ${item} -o ${output_h})
endif ()
#当.cpp文件已经存在时,仅当.ui文件被更新了才重新生成.cpp文件
if (EXISTS ${output_cpp})
file(TIMESTAMP ${output_cpp} cpp_time)
if (ui_time GREATER cpp_time)
execute_process(COMMAND ${UIC_EXE_PATH} ${item} -o ${output_cpp})
endif ()
else ()
execute_process(COMMAND ${UIC_EXE_PATH} ${item} -o ${output_cpp})
endif ()
endforeach ()
endfunction()
get_ui_source(${UI_FILES}) #功能的使用
cmake函数 逻辑判断
log输出
这里简单介绍一下 log输出:message(FLAG "Log输出信息")
FLAG | 含义 |
---|---|
(无) | 重要消息 |
WARNING | CMake 警告, 会继续执行 |
AUTHOR_WARNING | CMake 警告 (dev), 会继续执行 |
SEND_ERROR | CMake 错误, 继续执行,但是会跳过生成的步骤 |
FATAL_ERROR | CMake 错误, 终止所有处理过程 |
message("Success")
message(STATUS "notify")
message(WARNING "var = ${var}")
message(AUTHOR_WARNING "list_var = ${list_var}")
函数
function
endfunction
函数
-
参数
ARGC
:表示传入参数的个数ARGV
:表示所有参数ARGV0
:表示第一个参数,ARGV1
、ARGV2
以此类推
-
定义
function(num_method n1 n2 n3) message("call num_method method") message("n1 = ${n1}") message("n2 = ${n2}") message("n3 = ${n3}") message("ARGC = ${ARGC}") message("arg1 = ${ARGV0} arg2 = ${ARGV1} arg3 = ${ARGV2}") message("all args = ${ARGV}") endfunction(num_method)
-
调用
num_method(1 2 3) # 调用num_method函数
逻辑判断
条件命令
布尔量
逻辑 真/假
true(1,ON,YES,TRUE,Y,非0的值)
false(0,OFF,NO,FALSE,N,IGNORE,NOTFOUND)
set(if_tap OFF) # 定义变量if_tap,值为false
set(elseif_tap ON) # 定义变量elseif_tap,值为ture
判断变量相等
判断变量是否相等
# 变量var等于mStr
var STREQUAL "mStr"
# 变量var不等于mStr
NOT var STREQUAL "mStr"
if else
if
elseif
else
elseif
和else
部分是可选的,也可以有多个elseif
部分,缩进和空格对语句解析没有影响
if(${if_tap})
message("if_tap not False")
elseif(NOT if_tap STREQUAL "mStr")
message("if_tap not mStr")
else(${if_tap}) # 可以不加入 ${if_tap}
message("else")
endif(${if_tap}) # 结束if,可以不加${if_tap}但习惯上会加
#endif() # 结束if
循环
跳出循环
以下命令用于结束循环
break() # 跳出整个循环
continue() # 继续当前循环
while
while(条件)
endwhile()
set(a "")
while(NOT a STREQUAL "xxx")
set(a "${a}x")
message(">>>>>>a = ${a}")
endwhile()
foreach
foreach
endforeach(item)
更像是遍历
- 指定元素
foreach(item 1 2 3)
- 指定范围
foreach(item RANGE 2)
0 1 2 - 指定范围,步距
foreach(item RANGE 1 6 2)
1 3 5 - 指定列表
foreach(item IN LISTS list_va)
# 指定元素
foreach(item 1 2 3)
message("1item = ${item}")
endforeach(item) # 结束for
# 指定范围
foreach(item RANGE 2) # RANGE 默认从0开始, 所以是:0 1 2
message("2item = ${item}")
endforeach(item)
# 指定范围,步距
foreach(item RANGE 1 6 2) # 1 3 5 每次跳级2
message("3item = ${item}")
endforeach(item)
# 指定列表
set(list_va3 1 2 3) # 列表
# foreach(item IN LISTS ${list_va3}) 没有报错,没有循环
foreach(item IN LISTS list_va3)
message("4item = ${item}")
endforeach(item)
常用流程
客制化配置
指定项目名,配置编译器,设置平台 …
cmake_minimum_required(VERSION 3.0)
project(cpptestProject LANGUAGES CXX)
set(CMAKE_C_FLAGS "-Wall -DWITH_OPENSSL -DWITH_DOM")
#add_compile_options(-g)
# 设置平台
# - 设置默认值为X86
# - 可通过 cmake -D BUILD_PLATFROM="aarch64" 命令行赋值为aarch64 表示为arm平台
set(BUILD_PLATFROM "x86" CACHE STRING "default")
if(BUILD_PLATFROM STREQUAL "x86")
message("构建x86平台")
# # set(CMAKE_C_COMPILER "/usr/bin/gcc")
elseif(BUILD_PLATFROM STREQUAL "aarch64")
message("构建aarch64平台")
# # set(CMAKE_C_COMPILER "/usr/bin/aarch64-linux-gnu-gcc")
else()
message("构建其他平台")
endif()
target目标
CMake 中生成的 目标 target 一般有:
add_executable
可执行文件add_library
库文件
因此处理需要指定源文件外,还需要指定 可执行文件名/库文件名。
指定目标
可执行文件 add_executable
# 指定生成可执行文件
# - a.out
add_executable(a.out main.cpp hello.cpp)
库 add_library
静态、动态
IMPORTED 代指需要导入的源文件
# 指定生成库文件
# - 指定生成静态库 lib模块名.a
add_library(模块名 STATIC IMPORTED)
add_library(test STATIC source1.cpp source2.cpp)
# - 指定生成动态库 lib模块名.so
add_library(模块名 STATIC IMPORTED)
add_library(test SHARED source1.cpp source2.cpp)
指定库名:test
STATIC
或SHARED
指定生成 动态库/静态库:
- 动态库的名称
lib库名.so
- 静态库的名称
lib库名.a
指定源文件见下一部分
指定源文件
# 指定单个源文件
add_library(test SHARED source1.cpp)
# 指定多个源文件
add_library(test SHARED source1.cpp source1.h source2.cpp)
add_library(test SHARED [[ native-lib.cpp
T1.cpp
T2.cpp
T3.cpp ]])
通过变量的引用/解引用,批量导入源文件
# 定义全局变量allCPP 为所有的.c .h .cpp文件
# # file 定义一个变量 allCPP
# # GLOB从源码树中收集源文件列表,从而可以使用 *.cpp *.c *.h`)
file(GLOB allCPP *.c *.h *.cpp)
# 批量导入源文件, 解析变量allCPP
add_library(
test
SHARED
native-lib.cpp # 单个源文件
[[ native-lib.cpp T1.cpp T2.cpp T3.cpp ]] # 多个源文件
${allCPP} # # 解析变量allCPP 批量导入源文件
)
查找三方库/包
环境变量
那么,有哪些库是可以通过查找得到的呢?
1、系统预装,直接获取即可。(默认设置了环境变量)
2、那些已经为其手动设置环境变量的库,例如:通过如下方式添加库路径到环境变量。
# 添加并保留之前的环境变量(为库文件设置C++环境变量, 自动寻找库文件)
# - C CMAKE_C_FLAGS 变量
# - C++ `CMAKE_CXX_FLAGS 变量
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L ${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")
3、使用相对路径
手动显式指明相对路径,例如获取交叉编译生成的libgetndk.so
/libgetndk.a
# 开始真正导入静态库
set_target_properties(getndk PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libgetndk.a)
动态库必须在jniLibs
下
Cmake会自动寻找
jniLibs
下的动态库(静态库可以随意一些)创建app/src/main/jniLibs
。在其下创建平台对应的文件夹
app/src/main/jniLibs/armeabi-v7a
# 开始真正导入动态库
set_target_properties(getndk PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}/libgetndk.so)
安卓中这个SDK中的相对路径是怎么选择的呢?
D:\Android\Sdk\ndk\21.4.7075529\build\cmake\system_libs.cmake
21.4.7075529
- 从
local.properties
获知NDK版本- 或者是当前的NDK版本
arm-linux-androideabi
- 我的手机是arm32
arm32 == arm-linux-androideabi
16
minSdkVersion
包 find_package
find_package
命令,寻找 系统中预安装的 包
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
add_executable(a.out main.cpp)
# 系统中预安装的 包/库
find_package(fmt REQUIRED)
target_link_libraries(myexec PUBLIC fmt::fmt)
为避免冲突,每个包都享有一个独立的名字空间,以 :: 的分割。可以指定要使用包中的哪几个库(组件):
find_package(TBB REQUIRED COMPONENTS tbb tbbmalloc REQUIRED)
target_link_libraries(myexec PUBLIC TBB::tbb TBB::tbbmalloc)
库 find_library
find_library
命令,寻找 系统中预安装的库:
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
add_executable(a.out main.cpp)
# 系统中预安装的 包/库
find_library( # Sets the name of the path variable.
log-lib # 之后就可以使用log-lib替代log了 ${log-lib}只会加载一次
getndk # 查找liblog.so的路径 —— NDK library 用于输出log
)
# 链接此库 到 总库 a.out
target_link_libraries(a.out PUBLIC getndk)
引用库
要使用库文件,需要能够找到库文件和对应的头文件,
- 通过
target_link_libraries
指定 库文件 - 通过
target_include_directories
指定 头文件(使用纯有文件时,链接头文件就已经完成了库的导入)
添加第三方库的常用命令
链接库 target_link_libraries
添加链接的常用命令
# 链接库文件
# 创建库以后,要在某个可执行文件中使用该库需要链接
find_package(Threads REQUIRED) # 查找系统预装的第三方库
target_link_libraries(myexec PUBLIC test) # 为 myexec目标库 链接 libtest.a库
Cmake会自动寻找
jniLibs
下的动态库(静态库可以随意一些)创建
app/src/main/jniLibs
。在其下创建平台对应的文件夹
app/src/main/jniLibs/armeabi-v7a
除了头文件搜索目录以外,还有这些选项,PUBLIC 和 PRIVATE 对他们同理:
- target_include_directories(myapp PUBLIC /usr/include/eigen3) # 添加头文件搜索目录
- target_link_libraries(myapp PUBLIC hellolib) # 添加要链接的库
- target_add_definitions(myapp PUBLIC MY_MACRO=1) target_add_definitions(myapp PUBLIC -DMY_MACRO=1) # 添加一个宏定义
- target_compile_options(myapp PUBLIC -fopenmp) # 添加编译器命令行选项
- target_sources(myapp PUBLIC hello.cpp other.cpp) # 添加要编译的源文件
以及可以通过下列指令(不推荐使用),把选项加到所有接下来的目标去:
- include_directories(/opt/cuda/include) # 添加头文件搜索目录
- link_directories(/opt/cuda) # 添加库文件的搜索路径
- add_definitions(MY_MACRO=1) # 添加一个宏定义
- add_compile_options(-fopenmp) # 添加编译器命令行选项
引入头文件/纯头文件库 target_include_directories
把纯头文件的第三方库的 include 目录或头文件下载下来,然后target_include_directories(项目名 PUBLIC/PRIVATE 头文件路径)
即可。
如果另一个 b.out 也需要用 hellolib 这个库的头文件,难道也得再指定一遍头文件搜索路径吗?
答:不需要,导出一次就够了。区别是根据 链接属性 的不同,是否能够认为该库的头文件添加到了系统路径。
链接属性PUBLIC/PRIVATE
:是否导出头文件(其他模块是否允许使用该模块的头文件)
- PUBLIC
- 引用该库的可执行文件 CMake 会自动添加这个路径
- 指定的路径会被视为与系统路径等价,通过
<模块名.h>
引用这个头文件。
- STATIC
- 不希望让引用 子模块的可执行文件自动添加这个路径
- 通过
<相对路径/模块名.h>
引用这个头文件。
缺点:函数直接实现在头文件里,没有提前编译,
- 需要重复编译同样内容,
- 编译时间长。
# # 根目录/CMakeList.txt
# course-master/01/12/CMakeLists.txt
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
# 指定编译目标target是可执行文件 a.out
add_executable(a.out main.cpp)
# # 指定头文件目录(指定头文件搜索路径)
# # 希望引用它的可执行文件,会自动添加这个路径
# # - PUBLIC 可执行文件中使用子目录下的模块,视为与系统路径等价 不需要指明路径
# # target_include_directories(模块名 PUBLIC .)
# # - PRIVATE 不希望引用模块的可执行文件自动添加这个路径
# # target_include_directories(模块名 PRIVATE .)
target_include_directories(a.out PUBLIC hellolib/include)
target_include_directories的链接属性
- 如果不希望让引用 hellolib 的可执行文件自动添加这个路径,把 PUBLIC 改成 PRIVATE 即可。
- PUBLIC PRIVATE 决定一个属性要不要在被 link 的时候传播。(其他模块是否允许使用该模块的头文件)
子模块 add_subdirectory
更友好的方式:作为 CMake 子模块引入 add_subdirectory
做法
-
把某个
具体库
的东西移到工程的根目录/具体库
文件夹下,其子目录也包含一个 CMakeLists.txt, 定义了 hellolib 的生成规则
-
add_subdirectory
添加子目录 后,定义的库就可以在链接后使用了
根目录/CMakeList.txt
# 项目Cmake
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
# 放置源码至该目录下文件夹hellolib作为子模块
add_subdirectory(hellolib)
# 指定编译目标target是可执行文件 a.out
add_executable(${CMAKE_SOURCE_DIR}/a.out main.cpp)
# 链接子模块到目标文件
target_link_libraries(a.out PUBLIC hellolib)
根目录/hellolib/CMakeList.txt
子模块,项目名-子文件夹
# 子模块CMake
# 添加库文件
# - 静态库 libhellolib.a
add_library(hellolib STATIC 模块.cpp)
# - 动态库 libhellolib.so
add_library(hellolib SHARED 模块.cpp)
# 避免重复指定搜索路径,引入头文件
target_include_directories(a.out PUBLIC hellolib/include)
如果另一个 b.out 也需要用 hellolib 这个库,难道也得再指定一遍搜索路径吗?
不需要,只需要定义 hellolib 的头文件搜索路径,引用他的可执行文件 CMake 会自动添加这个路径
示例
链接库/子目录 到总库总流程
-
导入C库的头文件
可以CMakeLists.txt的路径使用相对路径
include_directories(路径)
-
导入C库的库文件
- 环境变量
- 逐个导入
-
指定编译目标——总库
add_library
。指定源文件加入到编译目标
-
链接
添加NDK工具的库,到总库中
-
在默认路径下查找/找到NDK工具库,
find_library
动态库必须在
jniLibs
下Cmake会自动寻找
jniLibs
下的动态库(静态库可以随意一些)创建
app/src/main/jniLibs
。在其下创建平台对应的文件夹
app/src/main/jniLibs/armeabi-v7a
-
子目录的下的
CMakeLists.txt
, 添加到总库中add_subdirectory
-
链接库
target_link_libraries
-
# 最低支持的CMake版本,
# # 注意:这里并不能代表最终的版本,最终版本在app.build.gradle中设置的
cmake_minimum_required(VERSION 3.18.1)
# 当前工程名 (选择性设置),以前的旧版本是没有的
project("fmod_learning01")
# 3.1 定义变量 allCPP,
# # GLOB :从源树中收集源文件列表,用于批量导入文件,就可以开心的 *.cpp *.c *.h了
file(GLOB allCPP ${CMAKE_SOURCE_DIR}/cpp/*.cpp ${CMAKE_SOURCE_DIR}/cpp/*.h)
# 1. 导入头文件 可以CMakeLists.txt的路径使用相对路径
# # 可以根据${CMAKE_SOURCE_DIR}使用相对路径
include_directories(inc) # 导入CMakeLists.txt同目录下的inc文件夹下的头文件
# 2. 导入库文件 - 环境变量
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")
# 2. 导入库文件 - 逐个导入 可读性更强,但代码多
# 使用相对路径获取交叉编译生成的libgetndk.so getndk就是库名
## OFF=0=false ON=1=true
## set(isSTATIC OFF)
set(isSTATIC ON)
if(${isSTATIC})
# # 声明静态库
add_library(getndk STATIC IMPORTED) # IMPORTED标志是声明的库
# # 开始真正导入静态库
set_target_properties(getndk PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libgetndk.a)
# 3.将源文件,添加到生成自己的总库
# 我们创建/添加一个库(动态库SHARED,静态库STATIC)
add_library(
# 1.定义库名 - Sets the name of the library.
native-lib # 最终生成动态库的名称 lib库名.so lib库名.a
# 2. 指定生成 动态库/静态库.
STATIC # 指定生成 动态库 - .so
# 3. 指定库文件的源文件
# 源文件:把.cpp .h源文件编译成 libfmod_learning01.so 库
# # 1. 单个源文件
native-lib.cpp
# # 2. 多个源文件
[[ native-lib.cpp
T1.cpp
T2.cpp
T3.cpp ]]
# # 3. 批量导入源文件, 解析变量allCPP
${allCPP}
)
else(${isSTATIC})
# 声明动态库
add_library(getndk SHARED IMPORTED) # IMPORTED标志是声明的库
# # 开始真正导入动态库
set_target_properties(getndk PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}/libgetndk.so)]]
# 3.将源文件,添加到生成自己的总库
# 我们创建/添加一个库(动态库SHARED,静态库STATIC)
add_library(
# 1.定义库名 - Sets the name of the library.
native-lib # 最终生成动态库的名称 lib库名.so lib库名.a
# 2. 指定生成 动态库/静态库.
SHARED # 指定生成 动态库 - .so
# 3. 指定库文件的源文件
# 源文件:把.cpp .h源文件编译成 libfmod_learning01.so 库
# # 1. 单个源文件
native-lib.cpp
# # 2. 多个源文件
[[ native-lib.cpp
T1.cpp
T2.cpp
T3.cpp ]]
# # 3. 批量导入源文件, 解析变量allCPP
${allCPP}
)
endif(${isSTATIC})
# 4 将NDK工具中的库,添加到总库中
# 4.1 查找
# 导入日志打印的动态库(liblog.so)
# 查找一个 NDK工具中的 动态库(liblog.so)
# 总库的cpp代码就可以使用 android/log.h的库实现代码了
find_library( # Sets the name of the path variable.
# 日志打印的库
log-lib # 之后就可以使用log-lib替代log了 ${log-lib}只会加载一次
#log # 查找liblog.so的路径 —— NDK library 用于输出log
getndk # 链接此库 到 总库 libnative-lib.so
)
# 4.2 将NDK工具中的库,添加到总库中
# 4.3 子目录的下的CMakeLists.txt, 添加到总库中
#引入get子目录下的CMakeLists.txt
add_subdirectory(${CMAKE_SOURCE_DIR}/cpp/libget)
#引入count子目录下的CMakeLists.txt
add_subdirectory(${CMAKE_SOURCE_DIR}/cpp/libcount)
# 4. 链接 目标库/子目录下指定的CMakeLists.txt, 到总库
target_link_libraries( # Specifies the target library.
# 如果是静态库,会把 libgetndk.a 拷贝到 总库 libnative-lib.so
# 如果是动态库,在运行期间, 总库 libnative-lib.so 去加载 我们的 libgetndk.so
# 被链接的总库
native-lib # libnative-lib.so
# 链接进入的具体库们
fmod_learning01 # 对应的库名称 lib库名.so lib库名.a
# 链接目标库(具体的库)到总库
${log-lib}) # # 查找日志打印的库 只会加载一次
#log # # 而log,可能会重复的加载
# 3. 把具体的库,链接到libfmod_learning01.so动态库
fmod # fmod库
fmodL # fmodL库
getndk # 链接此静态/动态库 到 总库 libnative-lib.so
get # 子目录下 具体的库 链接到 libnative-lib.so里面去
count # 子目录下 具体的库 链接到 libnative-lib.so里面去
)