音视频开发之旅(65) -带着问题学习实践CMake

目录

  1. 使用CMake创建一个可执行程序
  2. 创建一个动/静态库,并以库方式使用
  3. 以源码方式引入第三方库,以多层目录方式使用
  4. 跨平台共用lib第三库,改变代码的层级结构
  5. 其他的一些小细节(项目实践中遇到的问题)
  6. 资料
  7. 收获

CMake是我们CPP开发中很基础也是很重要的环节,就像Java的ant、Gradle作用类似构建编译CPP代码。
关于系统性的CMake的学习资料也很多,我是通过 cmake实践 和[cmake-examples] (https://github.com/ttroy50/cmake-examples)进行学习实践的。建议先看下这些资料内容系统学习并进行实践、实践、实践,再看这篇文章。
这篇文章一方面是对学习实践的收获以“问题-解决方案”的形式进行总结,另外一方面也是对最近工作中使用Cmake遇到的问题和踩到的坑进行记录。

下面开启我们的CMake学习实践之旅。

一、使用CMake创建一个可执行程序

源代码文件

#include 
int main()
{
    printf("Hello World from t1 main\n");
    return 0;
}

CMakeLists.txt文件

//每个CMakeList都有自己的POJECT,通过该指令定义这个CMake编译的最终产物的名称。
PROJECT (HELLO)

//SET可以设置变量的值,这里是把源文件名称赋值给变量SRC_LIST,如果有多个源文件使用空格分开
SET(SRC_LIST helloworld.cpp)

//下面四条语句是通过MESSAGE打印出一些变量的值。
//其中_SOURCE_DIR、_BINARY_DIR为隐式变量,projectname即上面第一行定义的值。
//PROJECT_SROURCE_DIR、PROJECT_BINARY_DIR则是显式变量,作用和上面两个一样。
//由于Projectname可以通过第一条指令改变,如果使用隐式变量,也要做相应的修改,而显式变量则不用。
//PROJECT_SROURCE_DIR:源文件(也可以理解为CMAKE)所在的路径
//PROJECT_BINARY_DIR:生成的二进制文件产物的路径,一般采用build目录下分离源文件和产物文件,则此时对应的路径为build文件夹路径
MESSAGE(STATUS "This is BINARY DIR ${HELLO_BINARY_DIR}")
MESSAGE(STATUS "This is PROJECT BINARY DIR ${PROJECT_BINARY_DIR}")
MESSAGE(STATUS "This is SOURCE DIR ${HELLO_SOURCE_DIR}")
MESSAGE(STATUS "This is PROJECT SOURCE DIR ${PROJECT_SOURCE_DIR}")

//这个指令用于生成可执行文件,第一个参数是生产可执行文件产物的名称、第二个用于生成该可执行文件的源文件
ADD_EXECUTABLE(hello ${SRC_LIST}) # hello可以写成${PROJECT_NAME}

然后在命令行中执行 mkdir build && cd build && cmake .. && make
可以看到输出的MESSAGE信息以及生成的可执行文件

 mkdir build && cd build && cmake .. && make
...
-- This is BINARY DIR /xxx/cmake/cmake实践/t1/build
-- This is PROJECT BINARY DIR /xxx/cmake/cmake实践/t1/build
-- This is SOURCE DIR /xxx/cmake/cmake实践/t1
-- This is PROJECT SOURCE DIR /xxx/cmake/cmake实践/t1

[100%] Linking CXX executable hello
[100%] Built target hello

/xxx/.../build % ls
CMakeCache.txt      Makefile        hello
CMakeFiles      cmake_install.cmake

 ./hello 
Hello World from t1 main

小节小结:

  1. 学习三个指令:PROJECT、MESSAGE、ADD_EXECUTABLE
  2. 了解五个变量:PROJECT_SROURCE_DIR、PROJECT_BINARY_DIR、_SOURCE_DIR、_BINARY_DIR、PROJECT_NAME
  3. 通过Cmake构建了一个简单的可执行程序

二、创建一个动/静态库,并以库方式使用

源文件:helloworld.cpp

#include 
void func()
{
    printf("Hello World\n");
}

CMakeList.txt

SET(LIB_HELLOWORLD_SRC helloworld.cpp)


//ADD_LIBRARY(libname [SHARED|STATIC|MODULE] [EXCLUDE_FROM_ALL] source1 source2 .. sourceN)

//类型有三种SHARED|STATIC|MODULE
//SHARED:动态库 mac下生成的是dylib,linux下生成的是so
//STATIC:静态库
//MODULE:一般用不到

//这个指令用于生成二进制库文件,第一个参数是产物的名称;第二个参数是库的类型:动态库、静态库等;第三个参数是用于生成该产物的源文件
ADD_LIBRARY(hello SHARED ${LIB_HELLOWORLD_SRC})

//上面的那条指令通过SHARED生成的是动态库、而这条指令通过STATIC生成的是静态库
ADD_LIBRARY(hello_static STATIC ${LIB_HELLOWORLD_SRC})

//这条指令用于设定输出target产物的一些属性,需要四个参数
//第一个参数是 target的名称
//第二个参数是固定的关键词 PROPERTIES
//第三个参数是属性名称 就像Map的key一样
//第四个参数是该属性的设置的值
// 为了静态库和动态库的名称为同样的名称,使用PROPERTIES OUTPUT_NAME "XXX"
SET_TARGET_PROPERTIES(hello_static PROPERTIES  OUTPUT_NAME "hello")

//为了同时生成静态库和动态库,并且后者不清除前者,使用PROPERTIES CLEAN_DIRECT_OUTPUT 1
SET_TARGET_PROPERTIES(hello PROPERTIES  CLEAN_DIRECT_OUTPUT 1)
SET_TARGET_PROPERTIES(hello_static PROPERTIES  CLEAN_DIRECT_OUTPUT 1)

同样执行mkdir build && cd build && cmake .. && make
可以看到同时有动态库libhello.dylib和静态库libhello.a生成

[ 25%] Building CXX object lib/CMakeFiles/hello_static.dir/helloworld.o
[ 50%] Linking CXX static library libhello.a
[ 50%] Built target hello_static
[ 75%] Building CXX object lib/CMakeFiles/hello.dir/helloworld.o
[100%] Linking CXX shared library libhello.dylib
[100%] Built target hello
yangbin@yangbindeMacBook-Pro build % ls lib/
CMakeFiles      cmake_install.cmake libhello.dylib
Makefile        libhello.a

使用生成的动/静态库, 生产可执行文件
源文件:main.cpp

#include "helloworld.h"
int main()
{
    func();
    return 0;
}

CMakeLists.txt

ADD_EXECUTABLE(main main.cpp)
MESSAGE(STATUS "PROJECT_BINARY_DIR"${PROJECT_BINARY_DIR})
MESSAGE(STATUS "PROJECT_SOURCE_DIR"${PROJECT_SOURCE_DIR})
#LINK_DIRECTORIES(lib) # 如果该行写在ADD_EXECUTABLE  报错 ld: warning: directory not found for option '-Llib'

#使用动态库
#ADD_LIBRARY(hello SHARED IMPORTED)
#SET_TARGET_PROPERTIES(hello PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/lib/libhello.dylib) # 这里用../lib/libhello.dylib就是不行,提示link时找不到对应的库,使用${PROJECT_SOURCE_DIR}绝对路径来设置才可以

#使用静态库
ADD_LIBRARY(hello STATIC IMPORTED)
SET_TARGET_PROPERTIES(hello PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/lib/libhello.a) # 这里用../lib/libhello.a就是不行,提示link时找不到对应的库,使用${PROJECT_SOURCE_DIR}绝对路径来设置才可以
INCLUDE_DIRECTORIES(${PROJECT_SOURCE_DIR}/lib/include)


TARGET_LINK_LIBRARIES(main hello)

小节小结:

  1. 学习两个指令:ADD_LIBRARY、SET_TARGET_PROPERTIES
  2. 学习生产动态库和静态库的方式
  3. 使用动/静态库

三、以源码方式引入第三方库,以多层目录方式使用

上面一小节,我们学习时间了,通过动态库或者静态库的方式使用。而有些场景需要我们以源码的方式而不是动/静态的方式引入。同时为了保证代码的相互独立,以多层目录而不是在同一个目录中使用。比如:为了方便的调用、修改、调试第三方库的场景。这小节我们对其进行实践。

├── CMakeLists.txt
├── lib
│ ├── CMakeLists.txt
│ ├── helloworld.cpp
│ └── include
│ └── helloworld.h
└── main.cpp

代码不变,修改点在于CMakeList
外层CMakeList.txt

ADD_EXECUTABLE(main main.cpp)
//这里用到了一个新的指令 添加子文件夹,有子文件夹的CmakeList来进行编译成库给外层使用
//ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

//这个指令用于向当前工程添加存放源文件的子目录,并且可以通过第二个参数指定中间二进制和目标二进制存放的位置。EXCLUDE_FROM_ALL参数的含义是将这个目录从编译过程中排出,比如工程中的test或者sample目录,可能需要工程构建完成后,再进入对应的目录单独构建
ADD_SUBDIRECTORY(lib)


//下面两个指令,都是添加头文件,但是使用方式和作用还是有些不同的。

//主要区别在于:
//1. include_directories 将作用于整个工程,target_include_directories 将作用于target 的项目
//2. target目标文件必须已经存在(由命令add_executable()或add_library()所创建),并且不能被IMPORTED修饰
//3. 关键字INTERFACE,PUBLIC和PRIVATE指定它后面参数的作用域。PRIVATE和PUBLIC项会填充targe目标文件的INCLUDE_DIRECTORIES属性。
//4 PUBLIC和INTERFACE项会填充target目标文件的INTERFACE_INCLUDE_DIRECTORIES属性。随后的参数指定包含目录。

//target_include_directories( [SYSTEM] [AFTER|BEFORE]  [items1…])
TARGET_INCLUDE_DIRECTORIES(main PUBLIC lib/include)

//include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 …])
#INCLUDE_DIRECTORIES(lib/include)

TARGET_LINK_LIBRARIES(main hello)

内层子文件夹的CMakeList.txt

SET(LIB_HELLOWORLD_SRC helloworld.cpp)

ADD_LIBRARY(hello SHARED ${LIB_HELLOWORLD_SRC})
INCLUDE_DIRECTORIES(include)

小节小结:

  1. 学习实践三个指令:ADD_SUBDIRECTORY、INCLUDE_DIRECTORIES、TARGET_INCLUDE_DIRECTORIES
  2. 以子文件夹的结构上组织代码的形式进行使用

四、跨平台共用lib第三库,改变代码的层级结构

如果代码结构发生变化,把lib不放到src的子文件夹下,有什么差异呐
这种场景的应用也很多,比如 lib是一些通用的跨平台库,而src是android
或者ios等平台的一些特有的代码。为了方便的公用lib就会采用这种组织形式

├── lib
│ ├── CMakeLists.txt
│ ├── helloworld.cpp
│ └── include
│ └── helloworld.h
└── src
├── CMakeLists.txt
└── main.cpp

Camke .. 时会报如下错误。

CMake Error at CMakeLists.txt:3 (ADD_SUBDIRECTORY):
  ADD_SUBDIRECTORY not given a binary directory but the given source
  directory "/xxx/cmake实践/t6/lib"
  is not a subdirectory of
  "/xxx/cmake实践/t6/src".  When
  specifying an out-of-tree source a binary directory must be explicitly
  specified.

原因是也很明显,如果文件结构上ADD_SUBDIRECTORY的文件夹不是target的子文件,则需要第二个参数指明,该子target生成的二进制产物的路径。

修改点:外层CmakeList

ADD_EXECUTABLE(main main.cpp)

//如果outputs文件夹不存在,则创建
file(MAKE_DIRECTORY output)

//添加第二个参数指明编译该子target的产物的存放位置
ADD_SUBDIRECTORY(../lib output)
TARGET_INCLUDE_DIRECTORIES(main PUBLIC ../lib/include)
#INCLUDE_DIRECTORIES(../lib/include)

TARGET_LINK_LIBRARIES(main hello)

小节小结:

  1. 改变代码的层级结构,更好的跨屏台支持。

五、其他的一些小细节(项目实践中遇到的问题)

如何使用CMake调用外部的工具库
如果子target通过SET_TARGET_PROPERTIES的OUTPUT_NAME属性设置了输出的library的名称,如下所示:

ADD_LIBRARY(hello_static STATIC ${LIB_HELLOWORLD_SRC})

SET_TARGET_PROPERTIES(hello_static PROPERTIES  OUTPUT_NAME "hello")

在目标target中使用时最好采用ADD_LIBRARY时的命名(即hello_static),而不是OUTPUT_NAME后的名称(即hello),否则link时找不到,在一些IDE(比如androidstuido或者clion)上编译会报错,特别是androidstudio不直接提示子target的产物找不到,而是子target和目标target并行编译了。。。

Message为什么有时打印不出来
这个也可能和平台兼容性有关系,在电脑端或者ios端都没问题,而android上却不一定能正常输出。需要设置为大于等于WARNING级别才可以。STATUS级别andorid上无法输出对应log

//android平台上无法输出该log,但是其他平台都可以
MESSAGE(STATUS "PROJECT_SOURCE_DIR"${PROJECT_SOURCE_DIR})

//andorid平台也可以输出
MESSAGE(WARNING "PROJECT_SOURCE_DIR"${PROJECT_SOURCE_DIR})

本文实践的代码已经上传到github-mediajourney

六、资料

[练习项目-cmake-examples-Chinese ]
练习项目-cmake-examples

图书-cmake实践.pdf
图书-CMake菜谱(CMake Cookbook中文版)

七、收获

通过本篇的学习实践,

  1. 了解Cmake的一些常用指令
  2. 通过Cmake进行工程化实践(以库、源码、跨屏的组织形式等多个角度实践)
  3. 总结记录实践中遇到的问题

感谢你的阅读
欢迎关注公众号“音视频开发之旅”,一起学习成长。
欢迎交流

你可能感兴趣的:(音视频开发之旅(65) -带着问题学习实践CMake)