CMake---優雅地構建軟件項目實踐(1)

本文屬於原創,轉載註明出處,歡迎關注微信小程序小白AI博客 微信公衆號小白AI或者網站 https://xiaobaiai.net 或者個人CSDN http://blog.csdn.net/freeapelinux

[TOC]android

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

1 咱們須要知道的基礎

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

  • 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就無法搜索到了,可設置:github

    • 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 咱們要優雅作到的構建

對於一個較大的軟件項目,咱們會依賴不少第三方的項目,包括源碼依賴或者庫依賴,而後完整的構建本身的軟件項目,則須要去構建依賴項目或者找到咱們所須要庫;另外,軟件項目會考慮到可移植性,即可以在不一樣的平臺上也可以很好友的去構建項目以及將項目轉移到另外一個開發環境時可以快速的開始構建。docker

除了上面所說的,咱們還須要考慮咱們實際軟件項目的架構結構,源碼結構,可讓開發人員更清晰的、更快速的瞭解整個項目。小程序

除此以外,C/C++ 程序員長期以來手動管理依賴,即手動查找、安裝依賴,再配置構建工具(如 cmake)使用依賴。 cmake 還提供了一系列 find_package 方法幫助簡化配置依賴, cmake 還支持多項目/模塊管理,若是依賴源碼同時被 cmake 管理構建,那麼狀況會簡單不少,這種方式稱爲源碼級依賴管理。 隨着代碼管理工具 git 出現並被普遍使用,git submodule 提供了一種不錯的源碼級依賴管理辦法。微信小程序

綜上,優雅的構建軟件項目,咱們實現:bash

  • 軟件項目源碼依賴第三方項目
  • 軟件項目庫依賴第三方項目
  • 軟件項目結構清晰
  • 軟件項目構建在轉換新環境下快速實現構建
  • 軟件項目構建過程當中的信息友好展現
  • 軟件項目構建完成後打包發佈
  • 軟件項目支持跨平臺構建
  • 軟件項目支持交叉構建
  • 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 參考資料

相關文章
相關標籤/搜索