CMake—優雅的構建C/C++軟體項目實踐(1)

  • 2020 年 3 月 31 日
  • 筆記

首先說明的是本篇文章不從cmake的整個語法上去講述,而是從一個實際項目的構建上入手,去了解如何優雅的去構建一個軟體項目,搭建一個C/C++軟體項目基本的依賴組件,最後形成一個構建C/C++軟體項目的模板,方便後面新項目的重複使用。相信對我們日常的軟體項目構建都會有很好的收穫。廢話不都說,開始。

1 我們需要知道的基礎

首先熟悉cmake的一些基操,我們就可以信手捏來的、優雅去構建一個項目,避免踩到不必要的坑。涉及到的有:

  • cmake的變數作用域?
  • cmake中的數據結構?
  • 宏函數與函數?
  • 如何去構建動靜態庫和找到這些庫?
  • 如何去實現支援多平台的項目構建?
  • 如何去構建一個應用?
  • 如何實現項目的最後install?
  • 如何很友好的去展示構建過程的各種級別資訊?
  • 如何適配cmake-gui,採用友好的ccmake或者cmake-gui實現構建?

這裡概括性說明下常用的cmake知識,總的來說cmake的作用就是讓我們找到依賴的頭文件和庫文件,去編譯源文件、鏈接目標文件(靜態庫也是目標文件的一個集合),最後生成可執行文件或動/靜態庫:

  • INCLUDE_DIRECTORIES 將給定的目錄添加到編譯器用於搜索包含文件(如頭文件)的目錄中,相對路徑被解釋為相對於當前源目錄。注意目錄僅是被添加到當前CMakeLists文件,作用於當前CMakeLists文件相關的庫、可執行文件或者子模組編譯,對於兩個不同CMakeLists.cmake並列的作用是無效的。區別於TARGET_INCLUDE_DIRECTORIES,這個命令的作用只是作用於指定的目標,為指定的目標添加搜索路徑。類似的還有TARGET_LINK_LIBRARIES命令(添加需要鏈接的庫文件目錄)。
  • PROJECT_SOURCE_DIR: 無疑只要是有包含最新PROJECT()命令聲明的CMakeLists.txt,則都是相對當該CMakeLists.txt路徑。
  • CMAKE_SOURCE_DIR: 構建整個項目時,可能你依賴的第三方項目,這個變數的值就是最頂層CMakeLists.txt的路徑。
  • find_pathfind_library以及 find_package 時,會搜索一些默認的路徑。當我們將一些lib安裝在非默認搜索路徑時,cmake就沒法搜索到了,可設置:
    • SET(CMAKE_INCLUDE_PATH "include_path") // find_path,查找頭文件
    • SET(CMAKE_LIBRARY_PATH "lib_path") // find_library,查找庫文件
    • SET(CMAKE_MODULE_PATH "module_path") // find_package
  • 尋找3rdparty也不一定需要自己去編寫FindXX.cmake,也可以直接用include(xxx.cmake)結合find_file命令實現尋找依賴庫,find_file尋找到的結果存放到CACHE變數,示例:
# Once done, this will define  #  #  NANOMSG_INCLUDE_DIR - the NANOMSG include directory  #  NANOMSG_LIBRARY_DIR - the SPDLOG library directory  #  NANOMSG_LIBS - link these to use NANOMSG  #  #  SPDLOG_INCLUDE_DIR - the SPDLOG include directory  #  SPDLOG_LIBRARY_DIR - the SPDLOG library directory  #  SPDLG_LIBS - link these to use SPDLOG    MACRO(LOAD_LIBNANOMSG os arch)      SET(3RDPARTY_DIR ${PROJECT_SOURCE_DIR}/3rdparty/target/${${os}}_${${arch}})      MESSAGE(STATUS "3RDPARTY_DIR: ${3RDPARTY_DIR}")      FIND_FILE(NANOMSG_INCLUDE_DIR include ${3RDPARTY_DIR} NO_DEFAULT_PATH)      FIND_FILE(NANOMSG_LIBRARY_DIR lib ${3RDPARTY_DIR} NO_DEFAULT_PATH)        SET(NANOMSG_LIBS          nanomsg          pthread          anl          PARENT_SCOPE      )      IF(NANOMSG_INCLUDE_DIR)          MESSAGE(STATUS "NANOMSG_LIBS : ${NANOMSG_LIBS}")      ELSE()          MESSAGE(FATAL_ERROR "NANOMSG_LIBS not found!")      ENDIF()  ENDMACRO()  
  • 條件控制切換示例:
# set target  if (NOT YOUR_TARGET_OS)      set(YOUR_TARGET_OS linux)  endif()    if (NOT YOUR_TARGET_ARCH)      set(YOUR_TARGET_ARCH x86_64)  endif()    if (NOT YOUR_BUILD_TYPE)      set (YOUR_BUILD_TYPE Release)  endif()    ......    if(${YOUR_TARGET_ARCH} MATCHES "(arm*)|(aarch64)")      ......  elseif(${YOUR_TARGET_ARCH} MATCHES x86*)      ......  
  • 交叉編譯: CMAKE_TOOLCHAIN_FILE變數,
MESSAGE(STATUS "Configure Cross Compiler")    IF(NOT TOOLCHAIN_ROOTDIR)      MESSAGE(STATUS "Cross-Compiler defaut root path: $ENV{HOME}/Softwares/arm-himix200-linux")      SET(TOOLCHAIN_ROOTDIR "$ENV{HOME}/Softwares/arm-himix200-linux")  ENDIF()    SET(CMAKE_SYSTEM_NAME Linux)  SET(CMAKE_SYSTEM_PROCESSOR arm)    SET(CMAKE_C_COMPILER       ${TOOLCHAIN_ROOTDIR}/bin/arm-himix200-linux-gcc)  SET(CMAKE_CXX_COMPILER     ${TOOLCHAIN_ROOTDIR}/bin/arm-himix200-linux-g++)    # set searching rules for cross-compiler  SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)  SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)  SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)    SET(YOUR_TARGET_OS linux)  SET(YOUR_TARGET_ARCH armv7-a)    SET(CMAKE_CXX_FLAGS "-std=c++11 -march=armv7-a -mfloat-abi=softfp -mfpu=neon-vfpv4 ${CMAKE_CXX_FLAGS}")  
  • AUX_SOURCE_DIRECTORY 不會遞歸包含子目錄,僅包含指定的dir目錄
  • ADD_SUBDIRECTORY子模組的編譯,可以將子文件夾中或者指定外部文件夾下CMakeLists.txt執行相關編譯工作。
  • ADD_LIBRARY編譯一個動/靜態庫或者模組,設定的名字需在整個工程中是獨一無二的,而且在整個同一個工程中,跟父子文件夾路徑無關,我們便可以通過TARGET_LINK_LIBRARIES依賴該模組。
  • ADD_DEFINITIONS(-DTEST -DFOO="foo")添加FOOTEST宏定義。

2 我們要優雅做到的構建

對於一個較大的軟體項目,我們會依賴很多第三方的項目,包括源碼依賴或者庫依賴,然後完整的構建自己的軟體項目,則需要去構建依賴項目或者找到我們所需要庫;另外,軟體項目會考慮到可移植性,即能夠在不同的平台上也能夠很好友的去構建項目以及將項目轉移到另一個開發環境時能夠快速的開始構建。

除了上面所說的,我們還需要考慮我們實際軟體項目的架構結構,源碼結構,可以讓開發人員更清晰的、更快速的了解整個項目。

除此之外,C/C++ 程式設計師長期以來手動管理依賴,即手動查找、安裝依賴,再配置構建工具(如 cmake)使用依賴。cmake 還提供了一系列 find_package 方法幫助簡化配置依賴, cmake 還支援多項目/模組管理,如果依賴源碼同時被 cmake 管理構建,那麼情況會簡單很多,這種方式稱為源碼級依賴管理。隨著程式碼管理工具 git 出現並被廣泛使用,git submodule 提供了一種不錯的源碼級依賴管理辦法。

綜上,優雅的構建軟體項目,我們實現:

  • 軟體項目源碼依賴第三方項目
  • 軟體項目庫依賴第三方項目
  • 軟體項目結構清晰
  • 軟體項目構建在轉換新環境下快速實現構建
  • 軟體項目構建過程中的資訊友好展示
  • 軟體項目構建完成後打包發布
  • 軟體項目支援跨平台構建
  • 軟體項目支援交叉構建
  • git submodule & cmake管理/構建源碼級依賴

另外,我們還實現一個可復用的C/C++最小開發框架(這個到後續文章中講述):

  • 支援日誌記錄
  • 支援任務池/執行緒池
  • 支援常用相關基礎操作組件
    • 時間日期操作
    • 文件讀寫操作
  • 支援valgrind記憶體泄露檢查工具
  • 支援靜態程式碼檢查
  • 支援項目文檔自動化
  • …..

3 優雅的軟體項目結構模板

3.1 模板一

一個獨立的應用,應用模組之間是相互聯繫的,彼此難以分開,這樣簡單的將所有源文件放一起,頭文件放一起,這個對於不是很複雜的應用是很快速的去開始構建和源文件修改操作:

.  ├── 3rdparty  ├── cmake  ├── include  ├── src  ├── doc  ├── tests  ├── benchmarks  ├── docker  ├── CMakeLists.txt  

3.2 模板二

源文件與頭文件分功能模組存放,這種方式是比較簡單,但是如果成為其他項目的3rdparty,則需要在安裝上將頭文件分離出來,不能很方便的被其他項目直接引用,個人覺得適用於App類項目,而非SDK項目(比如nanomsg這個開源消息中間件庫就是將頭文件和源文件放一起,但是作為SDK供外部鏈接就不是很直接、很方便了,需要做install操作之後才可以或者是將頭文件搜索範圍設置到依賴項目的src級別,且src目錄下模組分類很明確):

├── 3rdparty      ├── submodule # 存放源碼依賴      ├── target # 存放庫依賴      ├── CMakeLists.txt      ├── cmake # 存放 find_package cmake文件  ├── cmake  ├── platforms  │   └── linux  │       └── arm.toolchain.cmake  ├── src      ├── moudle1          ├── source & include file      ├── moudle2          ├── source & include file      ├── ......  ├── doc  ├── tests  ├── samples  ├── benchmarks  ├── docker  ├── CMakeLists.txt  

3.3 模板三

該軟體項目可以分為很多模組,各個模組可以互相獨立,也可以組合在一起,典型的如opencv項目,當然這個也適用於應用項目,但是應用項目的話目錄結構太深,開發編輯上稍有不便:

├── 3rdparty  ├── cmake  ├── platforms  │   └── linux  │       └── arm.toolchain.cmake  ├── include  該目錄只是各功能模組頭文件的一個匯總包含  ├── modules      ├── moudle1          ├── src          ├── include      ├── moudle2      ├── ......  ├── doc  ├── tests  ├── samples  ├── benchmarks  ├── docker  ├── CMakeLists.txt  

4 優雅的軟體項目結構模板CMake實現

這裡我們只去實現模板二,其他模板大同小異。如上面模板章節所述,我們

4.1 目錄結構確定

.  ├── 3rdparty                # 第三方庫源碼依賴和庫依賴存放位置  │   ├── CMakeLists.txt      # 第三方庫源碼依賴編譯CMakeLists文件  │   ├── spdlog              # 源碼依賴示例項目spdlog(github可搜索)  │   └── target              # 庫依賴存放目錄  │       ├── linux_armv7-a   # 以平台和架構命名區分  │       │   ├── include     # 頭文件存放目錄  │       │   └── lib         # 庫文件存放目錄  │       └── linux_x86-64  │           ├── include  │           └── lib  ├── cmake                   # 存放項目相關的cmakem模組文件  │   ├── load_3rdparty.cmake  │   ├── messagecolor.cmake  │   ├── toolchain_options.cmake  │   └── utils.cmake  ├── CMakeLists.txt          # 項目根目錄CMakeLists文件,cmake入口文件  ├── conf                    # 項目配置文件存放目錄  ├── doc                     # 項目文檔存放目錄  ├── platforms               # 項目平台性相關內容存放目錄,包括交叉編譯  │   └── linux  │       └── arm.himix200.cmake  ├── README.md               # 項目說明  ├── scripts                 # 相關腳本存放目錄,包括持續集成和部署相關  ├── src                     # 項目源碼目錄  │   ├── CMakeLists.txt  │   ├── common  │   ├── logger  │   └── main  └── tests                   # 測試示例源碼存放目錄      ├── CMakeLists.txt      └── test_logger.cpp  

4.2 項目版本的管理

不管是SDK或者是APP項目,都會有一個版本,用來記錄軟體發布的每個節點。軟體版本可以方便用戶或者自己清楚的知道每個版本都有哪些內容的更新,可以對版本做出使用的選擇或者解決版本中遇到的bug。實現版本的管理,需要能夠在編譯過程中清楚的體現當前版本號,在軟體中也能夠獲取版本號。這裡版本編號的管理使用常見的major.minor(.patch)格式,major是最大的版本編號,minor為其次,patch對應著小版本里的修補程式級別。當有極大的更新時,會增加major的版號,而當有大更新,但不至於更新major時,會更新minor的版號,若更新比較小,例如只是bug fixing,則會更新patch的版號。版本號格式示例:v1.0v1.2.2等。

在優雅的構建軟體模板中,我們將版本資訊放置於src/common/version.hpp文件中:

註:所有的文件路徑都是相對項目根目錄而言。

#pragma once    // for cmake  // 用於在CMakeLists文件中解析用  // 0.1.0  #define HELLO_APP_VER_MAJOR 0  #define HELLO_APP_VER_MINOR 1  #define HELLO_APP_VER_PATCH 0    #define HELLO_APP_VERSION (HELLO_APP_VER_MAJOR * 10000 + HELLO_APP_VER_MINOR * 100 + HELLO_APP_VER_PATCH)    // for source code  // 用於在項目源碼中獲取版本號字元串  // v0.1.0  #define _HELLO_APP_STR(s) #s  #define HELLO_PROJECT_VERSION(major, minor, patch) "v" _HELLO_APP_STR(major.minor.patch)  

在CMakeLists模組文件中我們去解析該文件獲取版本號到CMake變數中,在cmake/utils.cmake添加宏函數:

FUNCTION(hello_app_extract_version)      FILE(READ "${CMAKE_CURRENT_LIST_DIR}/src/common/version.hpp" file_contents)      STRING(REGEX MATCH "HELLO_APP_VER_MAJOR ([0-9]+)" _  "${file_contents}")      IF(NOT CMAKE_MATCH_COUNT EQUAL 1)          MESSAGE(FATAL_ERROR "Could not extract major version number from version.hpp")      ENDIF()      SET(ver_major ${CMAKE_MATCH_1})        STRING(REGEX MATCH "HELLO_APP_VER_MINOR ([0-9]+)" _  "${file_contents}")      IF(NOT CMAKE_MATCH_COUNT EQUAL 1)          MESSAGE(FATAL_ERROR "Could not extract minor version number from version.hpp")      ENDIF()      SET(ver_minor ${CMAKE_MATCH_1})      STRING(REGEX MATCH "HELLO_APP_VER_PATCH ([0-9]+)" _  "${file_contents}")      IF(NOT CMAKE_MATCH_COUNT EQUAL 1)          MESSAGE(FATAL_ERROR "Could not extract patch version number from version.hpp")      ENDIF()      SET(ver_patch ${CMAKE_MATCH_1})        SET(HELLO_APP_VERSION_MAJOR ${ver_major} PARENT_SCOPE)      SET (HELLO_APP_VERSION "${ver_major}.${ver_minor}.${ver_patch}" PARENT_SCOPE)  ENDFUNCTION()  

在根目錄CMakeLists中調用版本宏:

CMAKE_MINIMUM_REQUIRED(VERSION 3.4)    #--------------------------------------------  # Project setting  #--------------------------------------------  INCLUDE(cmake/utils.cmake)  HELLO_APP_EXTRACT_VERSION()    PROJECT(HelloApp VERSION ${HELLO_APP_VERSION} LANGUAGES CXX)    MESSAGE(INFO "--------------------------------")  MESSAGE(STATUS "Build HelloApp: ${HELLO_APP_VERSION}")  

在後面的動靜態庫生成中就可以設定SOVERSION了,如:

SET_TARGET_PROPERTIES(MyLib PROPERTIES VERSION ${HELLO_APP_VERSION}                                            SOVERSION ${HELLO_APP_VERSION_MAJOR})  

這樣就會生成一個liMyLibr.so => liMyLib.so.0 => libMyLib.so.0.1.1的庫和相關軟鏈接。不過這個操作謹慎使用,因為在android平台jni依賴帶版本的庫是無法找到的。

4.3 第三方庫庫依賴

第三方庫依賴需要我們自己寫庫和頭文件查找函數,三方庫存放位置以平台和架構作為區分,目錄結構隨著工程的創建就基本不會改變了。庫發現宏函數如下示例:

# Once done, this will define  #  #  SPDLOG_INCLUDE_DIR - the SPDLOG include directory  #  SPDLOG_LIBRARY_DIR - the SPDLOG library directory  #  SPDLG_LIBS - link these to use SPDLOG  #  #  ......    MACRO(LOAD_LIBSPDLOG os arch)      SET(3RDPARTY_DIR ${PROJECT_SOURCE_DIR}/3rdparty/target/${${os}}_${${arch}})      MESSAGE(STATUS "3RDPARTY_DIR: ${3RDPARTY_DIR}")      FIND_FILE(SPDLOG_INCLUDE_DIR include ${3RDPARTY_DIR} NO_DEFAULT_PATH)      FIND_FILE(SPDLOG_LIBRARY_DIR lib ${3RDPARTY_DIR} NO_DEFAULT_PATH)        SET(SPDLOG_LIBS          spdlog          pthread          #PARENT_SCOPE no parent      )      IF(SPDLOG_INCLUDE_DIR)          SET(SPDLOG_LIBRARY_DIR "${SPDLOG_LIBRARY_DIR}/spdlog")          MESSAGE(STATUS "SPDLOG_INCLUDE_DIR : ${SPDLOG_INCLUDE_DIR}")          MESSAGE(STATUS "SPDLOG_LIBRARY_DIR : ${SPDLOG_LIBRARY_DIR}")          MESSAGE(STATUS "SPDLOG_LIBS : ${SPDLOG_LIBS}")      ELSE()          MESSAGE(FATAL_ERROR "SPDLOG_LIBS not found!")      ENDIF()  ENDMACRO()  

注意:如SPDLOG_LIBS變數如果宏函數在根目錄CMakeLists中調用,所以變數作用域可以作用到所有子目錄,如果不是在根目錄調用,則需要設置PARENT_SCOPE屬性。

在主CMakeLists中調用宏函數實現三方庫的資訊導入:

INCLUDE(cmake/load_3rdparty.cmake)    IF(NOT YOUR_TARGET_OS)      SET(YOUR_TARGET_OS linux)  ENDIF()  IF(NOT YOUR_TARGET_ARCH)      SET(YOUR_TARGET_ARCH x86-64)  ENDIF()  MESSAGE(STATUS "Your target os : ${YOUR_TARGET_OS}")  MESSAGE(STATUS "Your target arch : ${YOUR_TARGET_ARCH}")    LOAD_LIBSPDLOG(YOUR_TARGET_OS YOUR_TARGET_ARCH)  

4.4 第三方庫源碼依賴

如果你想依賴第三方項目源碼,一起編譯,則我們可以通過git submodule來管理第三方源碼,實現源碼依賴和它的版本管理。當然你可以不用git submodule,直接將源碼手動放入3rdparty目錄中。

添加一個git submodule:

# url為git項目地址  # path為項目存放目錄,可以多級目錄,目錄名一般為項目名稱  # git add <url.git> <path>  # 示例,執行後,會直接拉取項目源碼到3rdparty/spdlog目錄下,並創建.gitmodule在倉庫根目錄下  $ git submodule add  https://github.com/gabime/spdlog.git 3rdparty/spdlog  

還可以做到帶指定分支進行添加操作:

# 注意:命令需要在項目根目錄下執行,第一次會直接拉取源碼,不用update  $ git submodule add -b v1.x   https://github.com/gabime/spdlog.git 3rdparty/spdlog  $ git submodule update --remote  

最後的.gitmodules文件為:

[submodule "3rdparty/spdlog"]      path = 3rdparty/spdlog      url = https://github.com/gabime/spdlog.git      branch = v1.x  

實現三方項目源碼編譯(首先你依賴的三方項目源碼是支援CMake構建方式的),在3rdparty/CMakeLists.txt中編寫:

CMAKE_MINIMUM_REQUIRED(VERSION 3.4)  PROJECT(HiApp3rdparty)    ADD_SUBDIRECTORY(spdlog)  

在根目錄CMakeLists.txt中包含3rdparty中CMakeLists.txt,就可以編譯第三方庫了:

ADD_SUBDIRECTORY(3rdparty)  

通過TARGET_LINK_LIBRARIES就可以指定第三方項目名稱實現鏈接。

4.5 功能模組添加

4.5.1 功能模組編譯

比如我們要添加一個日誌模組,實現對spdlog項目的一個二次封裝,更好的在自己的項目中使用,那麼我們建立src/logger目錄,裡面新建logger.hpplogger.cppCMakeLists.txt三個文件,其中CMakeLists.txt內容是對該日誌模組實現編譯:

CMAKE_MINIMUM_REQUIRED(VERSION 3.4)    AUX_SOURCE_DIRECTORY(. CURRENT_DIR_SRCS)  ADD_LIBRARY(module_logger ${CURRENT_DIR_SRCS})  # SPDLOG_LIBS 為spdlog項目庫名稱  TARGET_LINK_LIBRARIES(module_logger ${SPDLOG_LIBS})  

然後在src/CMakeLists.txt中包含該日誌模組的編譯:

ADD_SUBDIRECTORY(logger)  

在根目錄CMakeLists.txt中包含子目錄src,從而實現功能模組的構建:

ADD_SUBDIRECTORY(src)  

註:為了演示,庫依賴和源碼依賴都是用的spdlog,這裡實現日誌模組的話需要選擇其中一種方式。

4.5.2 可執行文件編譯

如果我們需要實現可執行文件對日誌模組的調用,我們可以添加src/main/main.cpp文件,在src/CMakeLists.txt中添加對可執行文件的編譯:

# main app  SET(SRC_LIST ./main/main.cpp)    ADD_EXECUTABLE(HiApp ${SRC_LIST})  # 配置可執行文件輸出目錄  SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)  TARGET_LINK_LIBRARIES(HelloApp module_logger)  

當然,如果使用c++11的特性,我們可以專門創建一個cmake文件cmake/toolchain_options.cmake來配置編譯選項,在其中配置c++11編譯選項,並在主CMakeLists.txt中包含該cmake文件:

# compiler configuration  # 從cmake3.1版本開始才支援CMAKE_CXX_STANDARD配置項  IF(CMAKE_VERSION VERSION_LESS "3.1")      IF(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")          SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")      ENDIF()  ELSE()      SET(CMAKE_CXX_STANDARD 11)  ENDIF()  

4.6 測試樣例添加

測試樣例放於tests目錄,並在該目錄下建立CMakeLists.txt文件用於構建所有測試demo,並在主CMakeLists.txt下包含tests目錄:

CMAKE_MINIMUM_REQUIRED(VERSION 3.4)    PROJECT(Tests)    INCLUDE_DIRECTORIES(      ${SPDLOG_INCLUDE_DIR}      ${CMAKE_SOURCE_DIR}/src  )    LINK_DIRECTORIES(      ${SPDLOG_LIBRARY_DIR}  )    FILE(GLOB APP_SOURCES *.cpp)  FOREACH(testsourcefile ${APP_SOURCES})      STRING(REGEX MATCH "[^/]+$" testsourcefilewithoutpath ${testsourcefile})      STRING(REPLACE ".cpp" "" testname ${testsourcefilewithoutpath})      ADD_EXECUTABLE( ${testname} ${testsourcefile})      SET(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin/tests)      TARGET_LINK_LIBRARIES(${testname}          ${SPDLOG_LIBS}          module_logger          )  ENDFOREACH(testsourcefile ${APP_SOURCES})  

然後就可以在tests目錄下添加測試程式了,比如test_logger.cpp或者更多的測試demo,tests/CMakeLists.txt會自動將tests目錄下所有源文件逐個進行可執行文件生成構建。整個測試樣例的構建就完成了。

4.7 交叉編譯配置

CMake給我們提供了交叉編譯的變數設置,即CMAKE_TOOLCHAIN_FILE這個變數,只要我們指定交叉編譯的cmake配置文件,那麼cmake會導入該配置文件的中編譯器配置,編譯選項配置等。我們設計的交叉編譯工具鏈配置文件存放目錄在platforms/下,這裡我們使用華為海思的一個編譯工具,我們按類別命名,創建一個工具欄cmake配置文件platforms/linux/arm.himix200.cmake:

MESSAGE(STATUS "Configure Cross Compiler")  SET(CMAKE_SYSTEM_NAME Linux)  SET(CMAKE_SYSTEM_PROCESSOR arm)    SET(CMAKE_C_COMPILER       arm-himix200-linux-gcc)  SET(CMAKE_CXX_COMPILER     arm-himix200-linux-g++)    # set searching rules for cross-compiler  SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)  SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)  SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)    SET(YOUR_TARGET_OS linux)  SET(YOUR_TARGET_ARCH armv7-a)    SET(CMAKE_SKIP_BUILD_RPATH TRUE)  SET(CMAKE_SKIP_RPATH TRUE)    # set ${CMAKE_C_FLAGS} and ${CMAKE_CXX_FLAGS}flag for cross-compiled process  #SET(CROSS_COMPILATION_ARM himix200)  #SET(CROSS_COMPILATION_ARCHITECTURE armv7-a)    # set g++ param  # -fopenmp link libgomp  SET(CMAKE_CXX_FLAGS "-std=c++11 -march=armv7-a -mfloat-abi=softfp -mfpu=neon-vfpv4       -ffunction-sections       -fdata-sections -O2 -fstack-protector-strong -lm -ldl -lstdc++      ${CMAKE_CXX_FLAGS}")  

注意:交叉編譯工具鏈是需要在編譯主機上安裝好的。另外第三方庫庫依賴也需要對應編譯出工具鏈版本(源碼依賴除外)。

命令行執行交叉編譯:

$ mkdir build  $ cd build  $ cmake .. -DCMAKE_TOOLCHAIN_FILE=../platforms/linux/arm.himix200.cmake  $ make -j  

這樣就實現了交叉編譯,你也可以配置其他的交叉編譯工具鏈。

4.8 其他

4.8.1 cmake message命令顏色凸顯

我們還可以自定義初始化cmake構建的message命令列印顏色,可以方便快速的凸顯出錯誤資訊,我們可以創建一個文件cmake/messagecolor.cmake

IF(NOT WIN32)      STRING(ASCII 27 Esc)      SET(ColourReset "${Esc}[m")      SET(ColourBold  "${Esc}[1m")      SET(Red         "${Esc}[31m")      SET(Green       "${Esc}[32m")      SET(Yellow      "${Esc}[33m")      SET(Blue        "${Esc}[34m")      SET(MAGENTA     "${Esc}[35m")      SET(Cyan        "${Esc}[36m")      SET(White       "${Esc}[37m")      SET(BoldRed     "${Esc}[1;31m")      SET(BoldGreen   "${Esc}[1;32m")      SET(BoldYellow  "${Esc}[1;33m")      SET(BOLDBLUE    "${Esc}[1;34m")      SET(BOLDMAGENTA "${Esc}[1;35m")      SET(BoldCyan    "${Esc}[1;36m")      SET(BOLDWHITE   "${Esc}[1;37m")  ENDIF()    FUNCTION(message)      LIST(GET ARGV 0 MessageType)      IF(MessageType STREQUAL FATAL_ERROR OR MessageType STREQUAL SEND_ERROR)          LIST(REMOVE_AT ARGV 0)          _message(${MessageType} "${BoldRed}${ARGV}${ColourReset}")      ELSEIF(MessageType STREQUAL WARNING)          LIST(REMOVE_AT ARGV 0)          _message(${MessageType}          "${BoldYellow}${ARGV}${ColourReset}")      ELSEIF(MessageType STREQUAL AUTHOR_WARNING)          LIST(REMOVE_AT ARGV 0)          _message(${MessageType} "${BoldCyan}${ARGV}${ColourReset}")      ELSEIF(MessageType STREQUAL STATUS)          LIST(REMOVE_AT ARGV 0)          _message(${MessageType} "${Green}${ARGV}${ColourReset}")      ELSEIF(MessageType STREQUAL INFO)          LIST(REMOVE_AT ARGV 0)          _message("${Blue}${ARGV}${ColourReset}")      ELSE()          _message("${ARGV}")  ENDIF()  

在主CMakeLists.txt中導入該cmake文件,則可以改變message命令各個級別列印的顏色顯示。

4.8.2 Debug與Release構建

為了方便debug,我們在開發過程中一般是編譯Debug版本的庫或者應用,可以利用gdb調試很輕鬆的就可以發現錯誤具體所在。在主cmake文件中我們只需要加如下設置即可:

IF(NOT CMAKE_BUILD_TYPE)      SET(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Choose Release or Debug" FORCE)  ENDIF()    MESSAGE(STATUS "Build type: " ${CMAKE_BUILD_TYPE})  

在執行cmake命令的時候可以設置CMAKE_BUILD_TYPE變數值切換Debug或者Release版本編譯:

$ cmake .. -DCMAKE_BUILD_TYPE=Release  

4.8.3 構建後安裝

對於SDK項目,我們需要對外提供頭文件和編譯完成後的庫文件,就需要用到cmake提供的install命令了。

我們安裝需求是:

  • src目錄下的每個模組頭文件都能夠安裝,並按原目錄存放安裝
  • 庫文件安裝放於lib目錄下
  • 可執行文件包括測試文件放於bin目錄

首先模組頭文件的安裝實現均在src/{module}/CMakeLists.txt中實現,這裡是安裝目錄,並過濾掉.cpp或者.c文件以及CMakeLists.txt文件,以logger模組為例:

INSTALL(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}      DESTINATION ${CMAKE_INSTALL_PREFIX}/include      FILES_MATCHING      PATTERN "*.h"      PATTERN "*.hpp"      PATTERN "CMakeLists.txt" EXCLUDE      )  

注意:在UNIX系統上,CMAKE_INSTALL_PREFIX變數默認指向/usr/local,在Windows系統上,默認指向c:/Program Files/${PROJECT_NAME}

然後是庫文件的安裝,也相關ADD_LIBRARY命令調用後中實現:

INSTALL(TARGETS module_logger      ARCHIVE DESTINATION lib      LIBRARY DESTINATION lib      RUNTIME DESTINATION bin)  

最後是可執行文件的安裝,跟安裝庫是一樣的,添加到ADD_EXECUTABLE命令調用的後面,只是因為是可執行文件,屬於RUNTIME類型,cmake會自動安裝到我們設置的bin目錄,這裡以HelloApp為例:

INSTALL(TARGETS HelloApp      ARCHIVE DESTINATION lib      LIBRARY DESTINATION lib      RUNTIME DESTINATION bin)  

執行安裝命令:

$ make install DESTDIR=$PWD/install  

則會在相對當前目錄install/usr/local目錄下生成:

.  ├── bin  │   ├── HelloApp  │   └── test_logger  ├── include  │   ├── common  │   │   ├── common.hpp  │   │   └── version.hpp  │   └── logger  │       └── logger.hpp  └── lib      └── libmodule_logger.a  

至此,安裝完成。

5 總結

「工欲善其事,必先利其器」,把基礎築好,在軟體開發過程中也是很重要的,就如項目中需求明確一樣,本篇文章我把C/C++項目開發的整體框架形成一個模板,不斷總結改進,方便後續類似項目的快速開發。本篇文章也主要實現項目構建方面的內容,下一篇準備實現一個基本C/C++框架所必須的基礎模組,包括日誌模組、執行緒池、常用基礎功能函數模組、配置導入模組、單元測試、記憶體泄露檢查等。如有問題或者改進,一起來交流學習,最後歡迎大家關注我的公眾號小白AI,不打廣告,不為了寫而寫,只為了分享自己的學習過程^_^。

整個構建模板源碼可以在我的github上找到,歡迎star:https://github.com/yicm/CMakeCppProjectTemplate

6 參考資料

  • http://www.oolap.com/cxx-dependency-management-old
  • http://www.yeolar.com/note/2014/12/16/cmake-how-to-find-libraries/