音视频开发之旅(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 <stdio.h>
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打印出一些变量的值。
//其中<projectname>_SOURCE_DIR、<projectname>_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、<projectname>_SOURCE_DIR、<projectname>_BINARY_DIR、PROJECT_NAME
  3. 通过Cmake构建了一个简单的可执行程序

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

源文件:helloworld.cpp

#include <stdio.h>
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(<target> [SYSTEM] [AFTER|BEFORE] <INTERFACE|PUBLIC|PRIVATE> [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. 总结记录实践中遇到的问题

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容