美文网首页
Cmake详解

Cmake详解

作者: 白茫茫的大地 | 来源:发表于2020-12-06 15:10 被阅读0次

    一、Cmake语法详解

    1、什么是CMake

    • 在Android Studio 2.2及以上,构建原生库的默认工具是CMake
    • CMake是一个跨平台的构建工具,可以用简单的语句来描述所有平台的安装(编译过程)。能够输出各种各样的makefile或者project文件。CMake并不直接构建出最终的软件,而是产生其他工具的脚本(如makefile),然后再依据这个工具的构建方式使用。
    • CMake是一个比make更高级的编译配置工具,它可以根据不同的平台、不同的编译器,生成相应的makefile或vcproj项目,从而达到跨平台的目的。Android Studio利用CMake生成的是ninja。ninja是一个小型的关注速度的构建系统。我们不需要关心ninja的脚本,知道怎么配置CMake就可以了。
    • CMake其实是一个跨平台的支持产出各种不同的构建脚本的一个工具。

    2、CMake源文件

    • CMake的源码文件可以包含命令、注释、空格和换行。
    • 以CMake编写的源文件以CmakeLists.txt命名或以.cmake为扩展名。
    • 可以通过add_subdirectory()命令把子目录的CMake源文件添加进来。
    • CMake源文件中所有有效的语句都是命令,可以是内置命令或自定义的函数/宏命令。

    3、CMake注释

    # 单行注释
    
    #[[多行注释
    多行注释
    多行注释]]
    复制代码
    
    • 单行注释:#注释内容(注释从#开始到行尾结束)
    • 钓竿注释:可以使用括号来实现多行注释:#[[多行注释]]

    4、CMake变量

    # 声明变量:set(变量名 变量值)
    set(var 123)
    # 引用变量:message 命令用来打印
    message("var = ${var}")
    复制代码
    
    • CMake中所有变量都是string类型。可以使用set()unset()命令来声明或移除一个变量
    • 变量的引用:${变量名}

    5、CMake列表(lists)

    # 声明列表:set(列表名 值1 值2 ... 值N)
    # 或set(列表名 "值1;值2;...;值N")
    set(list_var 1 2 3 4 5)
    # 或者
    set(list_var2 "1;2;3;4;5")
    
    message("list_var = ${list_var}")
    复制代码
    
    • 列表也是字符串,可以把列表看作一个特殊的变量,这个变量有多个值。
    • 语法格式:set(列表名 值1 值2 ... 值N)set(列表名 "值1;值2;...;值N")
    • 列表的引用:${列表名}

    6、CMake流程控制-操作符

    类型 名称
    一元 EXIST,COMMAND,DEFINED
    二元 EQUAL,LESS,LESS_EQUAL,GREATER,GREATER_EQUAL,STREQUAL,STRLESS,

    STRLESS_EQUAL,STRGREATER,STRGREATER_EQUAL,VERSION_EQUAL,VERSION_LESS,
    VERSION_LESS_EQUAL,VERSION_GREATER,VERSION_GREATER_EQUAL,MATCHES |
    | 逻辑 | NOT,AND,OR |

    7、CMake流程控制-布尔常量值

    类型
    true 1,ON,YES,TRUE,Y,非0的值
    false 0,OFF,NO,FALSE,N,IGNORE,NOTFOUND,空字符串,
    以-NOTFOUND结尾的字符串

    8、CMake流程控制-条件命令

    set(if_tap OFF)
    set(elseif_tap ON)
    
    if(${if_tap})
        message("if")
    elseif(${elseif_tap})
        message("elseif")
    else(${if_tap})
        message("else")
    endif(${if_tap})
    复制代码
    
    • 语法格式:
    if(表达式)
        COMMAND(ARGS...)
    elseif(表达式)
        COMMAND(ARGS...)
    else(表达式)
        COMMAND(ARGS...)
    endif(表达式)
    复制代码
    
    • elseif和else部分是可选的,也可以有多个elseif部分,缩进和空格对语句解析没有影响。

    9、CMake流程控制-循环命令

    set(a "")
    while(NOT a STREQUAL "xxx")
        set(a "${a}x")
        message("a = ${a}")
    endwhile()
    复制代码
    
    • 语法格式:
    while(表达式)
        COMMAND(ARGS...)
    endwhile(表达式)
    复制代码
    
    • break()命令可以跳出整个循环。
    • continue()可以跳出当前循环。

    10、CMake流程控制-循环遍历

    1)格式一

    foreach(item 1 2 3)
        message("item = ${item}")
    endforeach(item)
    复制代码
    
    • 语法格式:
    foreach(循环变量 参数1 参数2 ... 参数N)
        COMMAND(ARGS...)
    endforeach(循环变量)
    复制代码
    
    • 每次迭代设置循环变量为参数。
    • foreach也支持break()和continue()命令跳出循环。

    2)格式二

    foreach(item RANGE 3)
        message("item = ${item}")
    endforeach(item)
    复制代码
    
    • 语法格式:
    foreach(循环变量 RANGE total)
        COMMAND(ARGS...)
    endforeach(循环变量)
    复制代码
    
    • 循环范围从0到total。

    3)格式三

    foreach(item RANGE 1 5 2)
        message("item = ${item}")
    endforeach(item)
    复制代码
    
    • 语法格式:
    foreach(循环变量 RANGE start stop step)
        COMMAND(ARGS...)
    endforeach(循环变量)
    复制代码
    
    • 循环范围从start到stop,循环增量为step。

    4)格式四

    set(list_var 1 2 3)
    
    foreach(item IN LISTS list_var)
        message("item = ${item}")
    endforeach(item)
    复制代码
    
    • foreach还支持对列表的循环。
    • 语法格式:
    foreach(循环变量 IN LISTS 列表)
        COMMAND(ARGS...)
    endforeach(循环变量)
    复制代码
    

    11、CMake自定义函数命令

    function(func x y z)
        message("call function func")
        message("x = ${x}")
        message("y = ${y}")
        message("z = ${z}")
        message("ARGC = ${ARGC}")
        message("arg1 = ${ARGV0} arg2 = ${ARGV1} arg3 = ${ARGV2}")
        message("all args = ${ARGV}")
    endfunction(func)
    
    func(1 2 3)
    复制代码
    

    ARGC:表示传入参数的个数。 ARGV:表示所有参数。 ARGV0:表示第一个参数,ARGV1ARGV2以此类推。

    • 自定义函数命令格式:
    function(<name> [arg1[arg2[arg3...]]])
        COMMAND()
    endfunction(<name>)
    复制代码
    
    • 函数命令调用格式:name(实参列表)

    12、CMake自定义宏命令

    macro(ma x y z)
        message("call macro ma")
        message("x = ${x}")
        message("y = ${y}")
        message("z = ${z}")
    endmacro(ma)
    
    ma(1 2 3)
    复制代码
    

    函数命令有自己的作用域。 宏命令跟调用者的作用域一样。

    • 自定义宏命令格式:
    macro(<name> [arg1[arg2[arg3...]]])
        COMMAND()
    endmacro(<name>)
    复制代码
    
    • 宏命令调用格式:name(实参列表)

    13、CMake变量的作用域

    • 全局层:cache变量,在整个项目范围可见,一般在set定义变量时,指定CACHE参数就能定义cache变量。
    • 目录层:在当前目录CMakeLists.txt中定义,以及在该文件包含的其他cmake源文件中定义的变量。
    • 函数层:在命令函数中定义的变量,属于函数作用域内的变量。

    变量查找优先级:函数层 优于 目录层目录层 优于 全局层,即:函数层 --> 目录层 --> 全局层

    二、CMakeList.txt文件详解

    1、CMakeLists.txt简析

    使用AndroidStudio3.4创建一个C/C++Support的项目,默认在app/src/main目录下会生成cpp目录,里面包含CMakeLists.txt和native-lib.cpp。以下代码为CMakeLists.txt去掉英文注释格式化后的内容:

    cmake_minimum_required(VERSION 3.4.1)
    
    # 添加一个库,根据native-lib.cpp源文件编译一个native-lib的动态库
    add_library(
        native-lib
        SHARED
        native-lib.cpp)
    
    # 查找系统库,这里查找的是系统日志库,并赋值给变量log-lib
    find_library(
        log-lib
        log)
    
    # 设置依赖的库(第一个参数必须为目标模块,顺序不能换)
    target_link_libraries(
        native-lib
        ${log-lib})
    复制代码
    
    • cmake_minimum_required:指定cmake最低支持的版本。这个命令是可选的,如果在CMakeLists.txt文件中使用了高版本特有的命令的话,就需要加上这条命令,来指定CMake的最低支持版本,如果CMake当前版本低于我们指定的版本就会报错。
    • aux_source_directory(. DIR_SRCS):查找当前目录所有源文件,并将源文件名称列表保存到DIR_SRCS变量;但不能查找子目录中的源文件。
    • find_library:查找系统库,默认会在AndroidNDK目录\platforms\android-xx\arch-arm\usr\lib下查找。

    2、常用命令-add_library

    1)添加一个库

    • 添加一个库文件,名为<name>
    • 指定STATIC,SHARED,MODULE参数来指定库的类型。STATIC:静态库,SHARED:动态库,MODULE:在使用dyld的系统有效,若不支持dyld,等同于SHARED。
    • EXCLUDE_FROM_ALL:表示该库不会被默认构建。
    • source1 source2 ... sourceN:用来指定库的源文件。
    add_library(<name> [STATIC | SHARED | MODULE]
     [EXCLUDE_FROM_ALL] source1 source2 ... sourceN)
    复制代码
    

    2)导入预编译库

    • 添加一个已存在的预编译库,名为<name>
    • 一般配合set_target_properties使用。
    add_library(<name> <SHARED|STATIC|MODULE|UNKNOW> IMPORTED)
    
    # 比如
    add_library(test SHARED IMPORTED)
    set_target_properties(
        test # 指明目标库名
        PROPERTIES IMPORTED_LOCATION # 指明要设置的参数
        库路径/${ANDROID_ABI}/libtest.so # 导入库的路径
    )
    复制代码
    

    3、常用命令-set

    设置CMake变量:

    # 设置可执行文件的输出路径(EXECUTABLE_OUTPUT_PATH是全局变量)
    set(EXECUTABLE_OUTPUT_PATH [output_path])
    
    # 设置库文件的输出路径(LIBRARY_OUTPUT_PATH是全局变量)
    set(LIBRARY_OUTPUT_PATH [output_path])
    
    # 设置C++编译参数(CMAKE_CXX_FLAGS是全局变量)
    set(CMAKE_CXX_FLAGS "-Wall std=c++11")
    
    # 设置源文件集合(SOURCE_FILES是本地变量即自定义变量)
    set(SOURCE_FILES main.cpp test.cpp ...)
    复制代码
    

    4、常用命令-include_directories

    设置头文件目录。

    # 可以用相对或绝对路径,也可以用自定义的变量值
    include_directories(./include ${MY_INCLUDE})
    复制代码
    

    相当于g++选项中的-l参数。

    5、常用命令-add_executable

    添加可执行文件:

    add_executable(<name> ${SRC_LIST})
    复制代码
    

    6、常用命令-target_link_libraries

    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)
    复制代码
    
    • 将若干库链接到目标库文件。
    • 链接的顺序应当符合gcc链接顺序规则,被链接的库放在依赖它的库的后面,即如果上面的命令中,lib1依赖于lib2,lib2又依赖于lib3,则在上面命令中必须严格按照lib1 lib2 lib3的顺序排列,否则会报错。

    7、常用命令-add_definitions

    为当前路径以及子目录的源文件加入由-D引入的define flag

    add_definitions(-DF00 -DDEBUG ...)
    复制代码
    

    通常用于添加编译参数

    8、常用命令-add_subdirectory

    用于添加子目录的CMake源文件:

    # sub_dir指定包含CMakeLists.txt和源码文件的子目录位置
    # binary_dir是输出路径,一般可以不指定
    add_subdirectory(sub_dir [binary_dir])
    复制代码
    

    如果当前目录下还有子目录时可以使用add_subdirectory,子目录中也需要包含有CMakeLists.txt。

    9、常用命令-file

    文件操作命令:

    # 将message写入filename文件中,会覆盖文件原有内容
    file(WRITE filename "message")
    # 将message写入filename文件中,会追加在文件末尾
    file(APPEND filename "message")
    # 从filename文件中读取内容并存储到var变量中,如果指定了numBytes和offset,
    # 则从offset处开始最多读numBytes个字节,另外如果指定了HEX参数,则内容会以十六进制形式存储在var变量中
    file(READ filename var [LIMIT numbytes] [OFFSET offset] [HEX])
    # 重命名文件
    file(RENAME <oldname> <newname>)
    # 删除文件,等于rm命令
    file(REMOVE [file1 ...])
    # 递归的执行删除文件命令,等于rm -r
    file(REMOVE_RECURSE [file1...])
    # 根据指定的url下载文件
    # timeout超时时间;下载的状态会保存到status中;下载日志会被保存到log;sum指定所下载文件预期的MD5值,如果指定会自动进行比对,
    # 如果不一致,则返回一个错误;SHOW_PROGRESS,进度信息会以状态信息的形式被打印出来
    file(DOWNLOAD url file [TIMEOUT timeout] [STATUS status] [LOG log] [EXPECTED_MD5 sum] [SHOW_PROGRESS])
    # 创建目录
    file(MAKE_DIRECTORY [dir1 dir2 ...])
    # 会把path转换为以unix的/开头的cmake风格路径,保存到result中
    file(TO_CMAKE_PATH path result)
    # 它会把cmake风格的路径转换为本地路径风格:windows下用"\",而unix下用"/"
    file(TO_NATIVE_PATH path result)
    # 将会为所有匹配查询表达式的文件生成一个文件list,并将该list存储进变量variable里,如果一个表达式指定了RELATIVE,返回的结果
    # 将会是相对于定路径的相对路径,查询表达式例子:*.cxx,*.vt?
    # NOTE:按照官方文档的说法,不建议使用file的GLOB指令来收集工程的源文件
    file(GLOB variable [RELATIVE path] [globbing expressions]...)
    复制代码
    

    使用这种方式来指定源文件的话,如果后面项目需要增加源文件,比如项目中原本有2个源文件a.c和b.c,后面新增一个c.c,如果我们直接增加c.c,然后进行编译是会报错的,因为CMakeLists文件没有改动,所以cmake不会重新生成makefile文件,所以我们需要简单的改动一下CMakeLists文件,可以在CMakeLists文件中添加一个空格,然后再编译,它就会生成makefile文件。

    9、常用命令-set_directory_properties

    设置某个路径的一种属性:

    set_directory_properties(PROPERTIES prop1 value1 prop2 value2)
    复制代码
    

    prop1,prop2代表属性,取值为:

    • INCLUDE_DIRECTORIES
    • LINK_DIRECTORIES
    • INCLUDE_REGULAR_EXPRESSION
    • ADDITIONAL_MAKE_CLEAN_FILES

    10、常用命令-set_property

    在给定的作用域内设置一个命名的属性:

    set_property(<GLOBAL |
                DIRECTORY [dir] |
                TARGET [target ...] |
                SOURCE [src1 ...] |
                TEST [test1 ...] |
                CACHE [entry1 ...]>
                [APPEND]
                PROPERTY <name> [value ...])
    复制代码
    

    PROPERTY 参数是必须的。第一个参数决定了属性可以影响的作用域:

    • GLOBAL:全局作用域
    • DIRECTORY:默认当前路径,也可以用[dir]指定路径。
    • TARGET:目标作用域,可以是0个或多个已有目标。
    • SOURCE:源文件作用域,可以是0个或多个源文件。
    • TEST:测试作用域,可以是0个或多个已有的测试。
    • CACHE:必须指定0个或多个cache中已有的条目。

    11、多个源文件处理

    cmake_minimum_required(VERSION 3.4.1)
    # 查找当前目录所有源文件,并将名称保存到 DIR_SRCS 变量
    # 不能查找子目录
    aux_source_directory(. DIR_SRCS)
    # 也可以使用
    # file(GLOB DIR_SRCS *.c *.cpp)
    
    add_library(
        native-lib
        SHARED
        ${DIR_SRCS})
    复制代码
    

    如果源文件很多,把所有文件一个个加入很麻烦,可以使用aux_source_directory命令或file命令,会查找指定目录下的所有源文件,然后将结果存进指定变量名。

    12、多目录多源文件处理

    cmake_minimum_required(VERSION 3.4.1)
    aux_source_directory(. DIR_SRCS)
    # 添加child子目录下的cmakelist
    add_subdirectory(child)
    
    add_library(
        native_lib
        SHARED
        ${DIR_SRCS})
    target_link_libraries(native-lib child)
    ----------------------------------------
    
    # child目录下的CMakeLists.txt:
    cmake_minimum_required(VERSION 3.4.1)
    aux_source_directory(. DIR_LIB_SRCS)
    add_library(
        child
        SHARED
        ${DIR_LIB_SRCS})
    复制代码
    
    • 主目录中的CMakeLists.txt中添加add_subdirectory(child)命令,指明本项目包含一个子项目child。并在target_link_libraries指明本项目需要链接一个名为child的库。
    • 子目录child中创建CMakeLists.txt,这里child编译为共享库。

    13、添加预编译库

    1)Android6.0版本以前

    假设我们本地项目引用了libimported-lib.so:

    cmake_minimum_required(VERSION 3.4.1)
    # 使用 IMPORTED 标志告知 CMake 只希望将库导入到项目中
    # 如果是静态库则将shared改为static
    add_library(imported-lib
                SHARED
                IMPORTED)
    
    # 参数分别为:库、属性、导入地址、库所在地址
    set_target_properties(
        imported-lib
        PROPERTIES
        IMPORTED_LOCATION
        <路径>/libimported-lib.so)
    
    aux_source_directory(. DIR_SRCS)
    add_library(
        native-lib
        SHARED
        ${DIR_SRCS})
    
    target_link_libraries(native-lib imported-lib)
    复制代码
    
    • 添加add_library命令,第一个参数是模块名,第二个参数SHARED表示动态库,STATIC表示静态库,第三个参数IMPORTED表示以导入的形式添加。
    • 添加set_target_properties命令设置导入路径属性。
    • import-lib添加到target_link_libraries命令参数中,表示native-lib需要链接imported-lib模块

    2)Android6.0版本以后

    在Android6.0及以上版本,如果使用上节的方法添加预编译动态库的话,会有问题,我们可以使用另外一种方式来配置:

    # set命令定义一个变量
    # CMAKE_C_FLAGS:c的参数,会传递给编译器
    # 如果是c++文件,需要用CMAKE_CXX_FLAGS
    # -L:库的查找路径
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -L[so所在目录")
    复制代码
    

    14、添加头文件目录

    为了确保CMake可以在编译时定位头文件,使用include_directories,相当于g++选项中的-I参数。这样就可以使用#include <xx.h>,否则需要使用#include "path/xx.h"

    cmake_minimum_required(VERSION 3.4.1)
    # 设置头文件目录
    include_directories(<文件目录>)
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -L[so所在目录]")
    aux_source_directory(. DIR_SRCS)
    add_library(
        native-lib
        SHARED
        ${DIR_SRCS})
    target_link_libraries(native-lib imported-lib)
    复制代码
    

    15、build.gradle配置

    可以在gradle中使用arguments设置一些配置:

    android{
        defaultConfig{
            cmake{
                // 使用的编译器clang/gcc
                // cmake默认就是 gnustl_static
                arguments "-DANDROID_TOOLCHAIN=clang","-DANDROID_STL=gnustl_static"
                // 指定cflags和cppflags,效果和cmakelist使用一样
                cFlags ""
                cppFlags ""
                // 指定需要编译的cpu架构
                abiFilters "armeabi-v7a"
            }
        }
        externalNativeBuild{
            cmake{
                // 指定CMakeLists.txt文件相对当前build.gradle的路径
                path "xxx/CMakeLists.txt"
            }
        }
    }
    复制代码
    

    三、实操

    官网:www.fmod.com/ SDK下载页面:www.fmod.com/download

    需要先注册登录后,才能下载

    1、库文件集成

    在fmod官网下载好 fmodstudioapi20000android.tar.gz 后,解压,找到 api/core目录,其中inc是fmod的头文件,lib是fmod预编译好的so和jar文件。

    1)集成so与jar

    image.png

    2)集成.h头文件

    image.png

    2、CMakeLists.txt文件配置:

    fmod提供的so文件属于预编译库,需要在自己的工程中集成fmod并使用,大概可分为2步骤:

    1. 指定头文件目录。方便在native-lib.cpp中可以使用include <fmod.hpp>
    2. 指定预编译库目录。让native-lib在编译时,可以成功链接fmod。
    # 指定cmake最小支持版本
    cmake_minimum_required(VERSION 3.4.1)
    
    # 设置头文件目录
    include_directories(${CMAKE_SOURCE_DIR}/inc)
    
    # 设置第三方so库路径(android6.0以后需要这样设置)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")
    
    # 添加一个库,根据native-lib.cpp源文件编译一个native-lib的动态库
    add_library(
            native-lib
            SHARED
            native-lib.cpp)
    
    # 查找系统库,这里查找的是系统日志库,并赋值给变量log-lib
    find_library(
            log-lib
            log)
    
    # 设置以来的库(第一个参数必须为目标模块,顺序不能换)
    target_link_libraries(
            native-lib
            fmod
            fmodL
            ${log-lib})
    复制代码
    

    ${CMAKE_SOURCE_DIR} 可以获取到CMakeLists.txt文件当前所在目录路径。

    3、build.gradle配置

    因为该工具只在模拟器上运行,所以只需要x86平台的so库文件,可以使用 abiFilters 进行过滤。

    手机一般都是arm平台,可根据实际情况修改abiFilters。

    android {
        defaultConfig {
            ...
            externalNativeBuild {
                cmake {
                    cppFlags ""
                    abiFilters "x86" // 指定本地库的cpu架构
                }
            }
            ndk {
                abiFilters "x86" // 指定第三方库的cpu架构
            }
        }
        externalNativeBuild {
            cmake {
                path "src/main/cpp/CMakeLists.txt"
                version "3.10.2"
            }
        }
    }
    复制代码
    

    4、编写native-lib.cpp

    使用日志输出fmod版本号:

    #include <jni.h>
    #include <string>
    #include <android/log.h>
    #include <fmod.hpp>
    
    using namespace FMOD;
    
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_lqr_cmakefmod_MainActivity_stringFromJNI(
            JNIEnv *env,
            jobject /* this */) {
        System *system;
        System_Create(&system);
        unsigned int version;
        system->getVersion(&version);
        __android_log_print(ANDROID_LOG_ERROR, "TEST", "FMOD Version: %08x", version);
    
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    复制代码
    

    最终,在触发stringFromJNI()方法后,就可以在控制台看到fmod版本号的输出了。

    相关文章

      网友评论

          本文标题:Cmake详解

          本文链接:https://www.haomeiwen.com/subject/ebmowktx.html