短视频SDK的编译设计与实现系列(一)——CMake基础入门

简介: 一、背景介绍 在开写之前,简单介绍一下我要写这篇文章的背景及出发的方向。我们团队主要是做端上的多媒体开发,而这里面有大量的代码都是用c++来写的,由于历史原因,原先的工程结构其交叉编译配置比较复杂,另外加上团队很多同学之前都是做端上业务开发的,本身对于c/c++的交叉编译不是特别熟悉,因此萌生了从实用角度梳理一篇能够帮大家快速扫盲的Cmake基础教程的想法,同时也是对我自己的一次学习总结。 既

一、背景介绍

在开写之前,简单介绍一下我要写这篇文章的背景及出发的方向。我们团队主要是做端上的多媒体开发,而这里面有大量的代码都是用c++来写的,由于历史原因,原先的工程结构其交叉编译配置比较复杂,另外加上团队很多同学之前都是做端上业务开发的,本身对于c/c++的交叉编译不是特别熟悉,因此萌生了从实用角度梳理一篇能够帮大家快速扫盲的Cmake基础教程的想法,同时也是对我自己的一次学习总结。

既然选择从项目实用性考虑(其实主要是我自己水平不够),下面的讲解内容可能并不一定完整,更多的是符合我们团队的目前使用到的一些特性。

二、什么是Cmake?

  你或许听过好几种 Make 工具,例如 GNU Make ,QT 的 qmake ,微软的 MS nmake,BSD Make(pmake),Makepp,等等。这些 Make 工具遵循着不同的规范和标准,所执行的 Makefile 格式也千差万别。这样就带来了一个严峻的问题:如果软件想跨平台,必须要保证能够在不同平台编译。而如果使用上面的 Make 工具,就得为每一种标准写一次 Makefile ,这将是一件让人抓狂的工作。
CMake就是针对上面问题所设计的工具:它首先允许开发者编写一种平台无关的 CMakeList.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 和工程文件,如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程。从而做到“Write once, run everywhere”。显然,CMake 是一个比上述几种 make 更高级的编译配置工具。

CMake的核心流程:

1、创建CMakeLists.txt文件,用来做编译配置,一般来说CMakeLists.txt里面需要指定target,无论是executable还是library,总之需要一个目标。
2、执行在有CMakeLists.txt的文件中执行cmake命令,生成对应平台的makefile文件;
3、使用make命令进行编译。
当然以上是简单的描述,实际生产环境的编译配置可能要复杂的多,我们可以在后面的示例中逐步学习。

 

常用概念介绍

1、Target:CMake编译需要依赖指定的Target,是要生成一个library还是一个可执行文件executable,总之要给一个Target,后面所有的操作都将用target作为目标物。

三、一个简单的CMake使用示例

开始看示例前,我们先思考几个问题:

  1. 怎么把一坨c++代码变成一个可执行文件或者是一个library?(大学时学习c/c++都是ide点个run按钮就直接搞定,自己从头搭建却不会)
  2. 如何将工程进行模块化划分(单指代码层面,这里不讲设计)?各模块之间如何依赖?
  3. 如何让我的工程能够使用(依赖)到已经编译好的第三方库?
  4. 如何动态的去控制一些编译选项?(比如我要能够在代码里访问版本号,而又不用每次去改代码,能够利用不同的平台特性,区分Android,iOS,mac,window等等)

相信以上几个问题,可能是编译小白常会问的几个问题(至少我没学之前就是会存在这些甚至更多的疑惑)。接下里来我们简单写一个示例,跟着示例大家先对CMake的使用有个体感,同样上面的问题答案也将在示例中逐步揭晓:

示例背景

外国友人要买苹果,但是他能支付的是美元,商贩在结算的时候最后给到外国友人的消费总价需要是美元单位的,因此商贩需要依赖一个汇率转换工具去将人民币转换为美元。

ps:大家忽略示例的代码结构及代码设计,只关注脚本即可。

工程结构

|---project
   |----------app_module    #这个工程是app的主工程,里面有一个商贩类,并且有个main程序入口。
   |----------second_module #这个工程是二方module,将以源码的方式被app_module依赖,里面有个表示外国友人的foreign类。
   |----------third_party   #这个是工程依赖的第三方库,示例中的汇率转换工具,将由银行提供,作为第三方库被集成,非源码依赖。

示例讲解

  1. 帮助银行生成一个汇率库,我们给这个库起个名字叫rate_util,发出去给商贩们使用。
    #声明所需cmake的最小版本号
    cmake_minimum_required(VERSION 3.11)
    
    #指定生成的target(一个银行工具库,可以用来做汇率转换)
    add_library(rate_util SHARED rate_util.cpp)
    
    #指定library输出目录
    set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/build/libs/${OS})
    #安装头文件到指定的目录
    install(FILES "rate_util.h" DESTINATION "${PROJECT_SOURCE_DIR}/build/inc")
    
    #如果是iOS平台,则生成framework
    if("${OS}" STREQUAL "iOS")
        set_target_properties(rate_util PROPERTIES FRAMEWORK TRUE)
    endif()

    前面我们讲到过,cmake需要指定Target,那么这里的target就是要生成的工具库,这里我们编译一个动态库,用add_library来指定target是一个库,用SHARED标记这个是一个动态库,然后指定要编译的源文件,这里只有一个就是rate_util.cpp。如果要生成可执行文件就是add_executable,后面生成main程序的时候会看到。

    接下来还要指明这个库输出到哪里,这里我们使用cmake内置的一个变量CMAKE_LIBRARY_OUTPUT_DIRECTORY,实际上从变量名大家也能看出来,这个表示生成library的输出目录,cmake还有很多其他的内置变量,实际上上面代码的PROJECT_SOURCE_DIR也是一个内置变量,关于cmake变量我们将在后面讲解。

    然后我们又指定了这个库的头文件安装位置,因为这个库要被其他人拿去使用,所以相应的头文件肯定也要跟着发不出去,这里我们使用了一个install命令,这是cmake的内置命令,就像add_library一样都是cmake的内置命令,关于cmake命令我们后面会讲解。

    实际上到这里一个基础的cmake配置就完成了,后面我们还加了点小菜,就是给这个target设置了一个属性,标记如果是iOS平台编译的话,要生成framework,而不是dylib这种动态库。至此,第一个问题就有了答案。(示例下载:bank.zip

  2. 银行的第三方库有了,接下来就要搭建我们的app工程了,我们就按照上面提到的工程结构来。 

    在开始搭建前先来点小甜点,开篇提到过cmake编译时需要依赖一个CMakeLists.txt配置文件,如果我们像第一步那样所有的配置,所有module的配置都写到一个CMakeLists.txt里面,是不是要疯掉。幸好cmake有个超级好用的内置命令叫add_subdirectory,顾名思义,就是可以用它来指定子目录,当然也就意味着子目录里必须也有一个CMakeLists.txt,这样我们就可以分级去写配置了,各个module的配置写在各自的CMakeLists.txt 里就好了。按照这个思路,我们的工程结构应该变成这样

    |---project
       |----------CMakeLists.txt#工程最外面入口的CMakeLists.txt 
       |----------app_module    #这个工程是app的主工程,里面有一个商贩类,并且有个main程序入口。
                  |---------CMakeLists.txt #app_module这个子module的CMakeLists.txt
       |----------second_module #这个工程是二方module,将以源码的方式被app_module依赖,里面有个表示外国友人的foreign类。
                  |----------CMakeLists.txt #second_module这个子module的CMakeLists.txt
       |----------third_party   #这个是工程依赖的第三方库,示例中的汇率转换工具,将由银行提供,作为第三方库被集成,非源码依赖。
                  |----------CMakeLists.txt#第三方库的总配置CMakeLists.txt
                  |----------bank
                             |---------CMakeLists.txt#银行这个第三方库的CMakeLists.txt
     
    实际上这个结构就是我们demo工程的结构,接下来我们逐个CMakeLists.txt分析。首先看project->CMakeLists.txt,也就是工程总入口的配置:
    # CMake最低版本号要求
    cmake_minimum_required (VERSION 3.11)
    
    #项目信息
    project(p)
    #添加一堆的子module
    add_subdirectory(app_module ${PROJECT_SOURCE_DIR}/build-cache/app_module/)
    add_subdirectory(second_module ${PROJECT_SOURCE_DIR}/build-cache/second_module/)
    add_subdirectory(third_party ${PROJECT_SOURCE_DIR}/build-cache/third_party/)
    
    配置很简单,声明了cmake最低版本号,指定了项目信息,然后就是添加了一堆子module,实际上啥活也没干,事情都分配给下面的小弟了,然后我们再看小弟们的CMakeLists.txt先看app_module->CMakeLists.txt,
  3. #声明cmake最低版本号
    cmake_minimum_required (VERSION 3.11)
    
    #声明要编译的target是一个可执行文件,target名字是main
    add_executable(main
            saler.cpp
            )
    
    #指定main要链接的其他target,实际上也就是这些target对应的库
    target_link_libraries(main
            foreign
            rate_util
            )
    
    #指定这个target的include路径,编译器将在这些路径中寻找头文件
    target_include_directories(main
            PUBLIC
            ${PROJECT_SOURCE_DIR}/third_party
            )
    
    #设置可执行文件的输出路径
    set(EXECUTABLE_OUTPUT_PATH   ${PROJECT_SOURCE_DIR}/build/bin/${OS})

这个里面就有货了,首先映入眼帘的就是add_executable,原来最终的可执行文件就是在这里生成的,定义了一个叫main的target,由于这个app主程序还要依赖外国友人和银行汇率工具库这两个库,所以我们要给它去指定link的库,这里用到cmake另一个内置命令target_link_libraries,但是参数里指定的都是target name,而并不是库名。只link了库还不够,还要指定头文件查找路径,否则第三方库的头文件找不到,用cmake内置命令target_include_directories,最后需要设置可执行文件的输出目录,这里类似步骤1中生成银行库一样,也是设置cmake内置变量的值,用到的内置变量是EXECUTABLE_OUTPUT_PATH,后面的${OS}是我们自定义的一个变量,去看完整的示例工程时会看到。接下来我们再看second_module->CMakeLists.txt,也就是外国友人所在的module,这个module并不是独立的第三方库,而是源码依赖的。

  1. #声明cmake最小版本号
    cmake_minimum_required (VERSION 3.11)
    #指定target,一个名字为foreign的target,生成物是一个静态库
    add_library(foreign
            STATIC
            ForeignFriend.cpp
            )
    #指定该target的头文件查找路径,PUBLIC表示该路径也会被传递给依赖该target的库,在该工程中app_module依赖了foreign,因此app_module也能访问该路径,这也就是
    #为什么app_module中的文件在引用foreign库中的头文件时不用加前面的路径,直接使用文件名即可
    target_include_directories(foreign
            PUBLIC
            ${CMAKE_CURRENT_LIST_DIR}
            )

可以看到foreign被作为另一个target声明了,指定生成一个名字为foreign的静态库,并且同样指明了头文件包含路径,由于这里是源码依赖的,该头文件不需要发布给外外面,因此不需要install。最后我们看下第三方库——银行汇率转换工具库是怎么被使用的。

  1. #声明cmake最小版本号
    cmake_minimum_required(VERSION 3.11)
    #添加一个imported library,
    add_library(rate_util SHARED IMPORTED
            GLOBAL)
    #设置library的位置
    set(__lib_name lib_rateutil.so)
    if("${OS}" STREQUAL "android")
        set(__lib_name librate_util.so)
    elseif("${OS}" STREQUAL "iOS")
        set(__lib_name rate_util.framework)
    else()
        set(__lib_name librate_util.dylib)
    endif()
    #设置target属性,指明该target imported的库的路径
    set_target_properties(rate_util PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/third_party/bank/libs/${OS}/${__lib_name})
    #由于这里target是imported进来的,不能使用target_include_directories,因此要用include_directories
    include_directories(rate_util
            PUBLIC
            ${CMAKE_CURRENT_LIST_DIR}/inc)

这里第三方库也被声明为一个单独的target了,但是与之前不同的是这里并没有给target指定源文件(因为本来也没有源文件,这是外部编译好的可以直接拿来使用的动态库),而是通过一个IMPORTED标记,表示这个target是外部引入的库,然后通过给target设置IMPORTED_LOCATION这一属性值,指明导入库的文件路径。另外有一点不同的是这里声明头文件包含路径时不能使用target_include_directories了,而要使用include_directries,cmake不允许为imported的库使用target_include_directries。至此,开始提出的2,3问题也都有了答案。

  3.  最后我们看一下如何生成不同平台的库

这里先了解一个概念,toolchain,也就是工具链的意思,实际上就是一个配置文件,内 部指定了cmake内置变量在目标平台下应该设置的参数值,比如CMAKE_SYSTEM_NAME,CMAKE_SYSROOT等等,我们选一个示例中Android平台的toolchain看一下。

  1. #导入NDK自带的toolchain文件
    include ($ENV{NDK_ROOT}/build/cmake/android.toolchain.cmake)

    #设置toolchain内部自定义的一些变量值
    set (ANDROID_TOOLCHAIN clang ) #指定编译器
    set (ANDROID_PLATFORM android-18) #指定Android平台版本号
    set (ANDROID_STL c++_static ) #指定使用的c++标准库

Android平台下的toolchain可以通过NDK自带的make-standalone-toolchain.sh工具来生成,具体如何使用这里不细讲了,包括其他平台的toolchain如何获取感兴趣的同学可以自行Google一下,由于我的示例工程是在mac系统上编写的,因此示例中mac系统的cmake没有单独去配置toolchain,因为在该系统下cmake默认就会使用该平台的环境变量,当然如果要回答开头提到的问题4,toolchain只能涵盖一部分,实际上通过宏也可以实现一些动态控制的作用,由于示例工程中没有使用到,这里不展开讲了,后面特性中会讲到。

示例下载:project.zip

  4.  总结

以上示例从单独编译生成可执行文件/链接库,依赖第三方导入库,依赖二方源码库,跨平台交叉编译等层面展示了cmake一些常用特性,相信看完这个示例大家对cmake的使用会有一个初步的了解了,下一节我们将针对示例中用到的cmake内置命令,cmake内置变量,自定义变量,宏,toolchain,等常用特性做细致的讲解。

四、附录——参考文章

相关文章
|
6月前
|
开发工具
使用FFmpeg4.3.1的SDK官方开发包编译ffmpeg.c(三)
使用FFmpeg4.3.1的SDK官方开发包编译ffmpeg.c(三)
92 0
|
算法 IDE 开发工具
火爆全网开源额温枪同平台之华大HC32L136 SDK开发入门
火爆全网开源额温枪同平台之华大HC32L136 SDK开发入门
253 1
|
开发工具 Android开发
【错误记录】Android Studio 编译报错 ( SDK location not found )
【错误记录】Android Studio 编译报错 ( SDK location not found )
2556 0
【错误记录】Android Studio 编译报错 ( SDK location not found )
|
开发工具
编译SDK出现aclocal-1.15: command not found错误
编译SDK出现aclocal-1.15: command not found错误
896 0
|
6月前
|
编解码 IDE 开发工具
使用FFmpeg4.3.1的SDK官方开发包编译ffmpeg.c(一)
使用FFmpeg4.3.1的SDK官方开发包编译ffmpeg.c(一)
53 1
|
6月前
|
开发工具
使用FFmpeg4.3.1的SDK官方开发包编译ffmpeg.c(二)
使用FFmpeg4.3.1的SDK官方开发包编译ffmpeg.c(二)
73 0
|
编解码 Linux 开发工具
瑞芯微RV1109 SDK编译&支持QT程序开发
瑞芯微RV1109 SDK编译&支持QT程序开发
710 0
|
存储 Ubuntu Linux
linphone android sdk 源码下载编译
linphone android sdk 源码下载编译
1156 0
linphone android sdk 源码下载编译
|
缓存 Kubernetes Java
openshift operator-sdk 入门
openshift operator-sdk 入门
openshift operator-sdk 入门
|
XML 存储 Ubuntu
RK3568开发笔记(五):在虚拟机上使用SDK编译制作uboot、kernel和ubuntu镜像
buildroot虽然灵活,但是基于实际情况,本身是侧重驱动和应用定制开发的只定制一次文件系统投入有点多,还不如直接ubunt自己交叉编译依赖库,做一些库的移植裁剪。   于是本篇就使用ubuntu系统了,至于其他库自己下源码在宿主机交叉编译号后,再拷贝过去或者直接在板子上编译也行(只是会比较慢),但是意义不大,因为开发过程肯定是用宿主机,不然核心板编译太慢,在编译上会花费不少可以省去的时间。
RK3568开发笔记(五):在虚拟机上使用SDK编译制作uboot、kernel和ubuntu镜像