音視頻開發之旅(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. 總結記錄實踐中遇到的問題

感謝你的閱讀
歡迎關注公衆號“音視頻開發之旅”,一起學習成長。
歡迎交流

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章