首頁 > 軟體

GNU開發工具:CMake快速入門教學

2020-06-16 16:45:43

一、CMake簡介

不同Make工具,如GNU Make、QT的qmake、微軟的MS nmake、BSD Make(pmake)等,遵循著不同的規範和標準,所執行的Makefile格式也不同。如果軟體想跨平台,必須要保證能夠在不同平台編譯。而如果使用Make工具,必須為不同的Make工具編寫不同的Makefile。
CMake是一個比Make工具更高階的編譯設定工具,是一個跨平台的、開源的構建系統(BuildSystem)。CMake允許開發者編寫一種平台無關的CMakeList.txt檔案來客製化整個編譯流程,然後再根據目標使用者的平台進一步生成所需的在地化Makefile和工程檔案,如:為Unix平台生成Makefile檔案(使用GCC編譯),為Windows MSVC生成projects/workspaces(使用VS IDE編譯)或Makefile檔案(使用nmake編譯)。使用CMake作為專案架構系統的知名開源專案有VTK、ITK、KDE、OpenCV、OSG等。

二、CMake管理工程

在Linux平台下使用CMake生成Makefile並編譯的流程如下:
A、編寫CMake組態檔CMakeLists.txt
B、執行命令cmake PATH生成Makefile,PATH是CMakeLists.txt所在的目錄。
C、使用make命令進行編譯。

三、單個原始檔工程

1、原始檔編寫

假設專案test中只有一個main.cpp原始檔,程式用途是計算一個數的指數冪。

#include <stdio.h>
#include <stdlib.h>
/**
 * power - Calculate the power of number.
 * @param base: Base value.
 * @param exponent: Exponent value.
 *
 * @return base raised to the power exponent.
 */
double power(double base, int exponent)
{
    int result = base;
    int i;

    if (exponent == 0)
    {
        return 1;
    }

    for(i = 1; i < exponent; ++i)
    {
        result = result * base;
    }
    return result;
}
int main(int argc, char *argv[])
{
    if(argc < 3)
    {
        printf("Usage: %s base exponent n", argv[0]);
        return 1;
    }
    double base = atof(argv[1]);
    int exponent = atoi(argv[2]);
    double result = power(base, exponent);
    printf("%g ^ %d is %gn", base, exponent, result);
    return 0;
}

2、編寫CMakeLists.txt

在main.cpp原始檔目錄test下編寫CMakeLists.txt檔案。

#CMake最低版本號要求
cmake_minimum_required (VERSION 2.8)
#專案資訊
project (demo)
#指定生成目標
add_executable(demomain.cpp)

CMakeLists.txt由命令、注釋和空格組成,其中命令是不區分大小寫。符號#後的內容被認為是注釋。命令由命令名稱、小括號和引數組成,引數之間使用空格進行間隔。
本例中CMakeLists.txt檔案的命令如下:
cmake_minimum_required:指定執行本組態檔所需的CMake的最低版本;
project:引數值是demo,表示專案的名稱是demo。
add_executable:將名為main.cpp的原始檔編譯成一個名稱為demo的可執行檔案。

3、編譯工程

在原始碼根目錄下建立一個build目錄,進入build目錄,執行cmake ..,生成Makefile,再使用make命令編譯得到demo可執行檔案。
通常,建議在原始碼根目錄下建立一個獨立的build構建編譯目錄,將構建過程產生的臨時檔案等檔案與原始碼隔離,避免原始碼被汙染。

四、單目錄多原始檔工程

1、原始檔編寫

假如把power函數單獨寫進一個名為MathFunctions.cpp的原始檔裡,使得這個工程變成如下的形式:

MathFunctions.h檔案:

/**
 * power - Calculate the power of number.
 * @param base: Base value.
 * @param exponent: Exponent value.
 *
 * @return base raised to the power exponent.
 */

double power(double base, int exponent);

MathFunctions.cpp檔案:

double power(double base, int exponent)
{
    int result = base;
    int i;

    if (exponent == 0)
    {
        return 1;
    }

    for(i = 1; i < exponent; ++i)
    {
        result = result * base;
    }
    return result;
}

main.cpp檔案:

#include <stdio.h>
#include <stdlib.h>
#include "MathFunctions.h"

int main(int argc, char *argv[])
{
    if(argc < 3)
    {
        printf("Usage: %s base exponent n", argv[0]);
        return 1;
    }

    double base = atof(argv[1]);
    int exponent = atoi(argv[2]);
    double result = power(base, exponent);
    printf("%g ^ %d is %gn", base, exponent, result);

    return 0;
}

2、編寫CMakeLists.txt

#CMake最低版本號要求
cmake_minimum_required(VERSION 2.8)
#專案資訊
project(demo)
#指定生成目標
add_executable(demomain.cpp MathFunctions.cpp)

add_executable命令中增加了一個MathFunctions.cpp原始檔,但如果原始檔很多,可以使用aux_source_directory命令,aux_source_directory命令會查詢指定目錄下的所有原始檔,然後將結果存進指定變數名。其語法如下:
aux_source_directory(dir variable)
修改後CMakeLists.txt如下:

#CMake最低版本號要求
cmake_minimum_required(VERSION 2.8)
# 專案資訊
project(demo)
#查詢當前目錄下的所有原始檔
#並將名稱儲存到DIR_SRCS變數
aux_source_directory(. DIR_SRCS)
#指定生成目標
add_executable(demo${DIR_SRCS})

CMake會將當前目錄所有原始檔的檔名賦值給變數DIR_SRCS ,再指示變數DIR_SRCS中的原始檔需要編譯成一個名稱為demo的可執行檔案。

五、多檔案多目錄工程

1、原始碼檔案編寫

建立一個math目錄,將MathFunctions.h和MathFunctions.cpp檔案移動到math目錄下。在工程目錄根目錄test和子目錄math裡各編寫一個CMakeLists.txt檔案,可以先將math目錄裡的檔案編譯成靜態庫再由main函數呼叫。

math子目錄:
MathFunctions.h檔案:

/**
 * power - Calculate the power of number.
 * @param base: Base value.
 * @param exponent: Exponent value.
 *
 * @return base raised to the power exponent.
 */

double power(double base, int exponent);

MathFunctions.cpp檔案:

double power(double base, int exponent)
{
    int result = base;
    int i;

    if (exponent == 0)
    {
        return 1;
    }

    for(i = 1; i < exponent; ++i)
    {
        result = result * base;
    }
    return result;
}

根目錄原始檔:

#include <stdio.h>
#include <stdlib.h>
#include "math/MathFunctions.h"

int main(int argc, char *argv[])
{
    if(argc < 3)
    {
        printf("Usage: %s base exponent n", argv[0]);
        return 1;
    }

    double base = atof(argv[1]);
    int exponent = atoi(argv[2]);
    double result = power(base, exponent);
    printf("%g ^ %d is %gn", base, exponent, result);

    return 0;
}

2、CMakeLists.txt檔案編寫

根目錄的CMakeLists.txt檔案:

# CMake最低版本號要求
cmake_minimum_required(VERSION 2.8)
# 專案資訊
project(demo)
#查詢當前目錄下的所有原始檔
#並將名稱儲存到DIR_SRCS變數
aux_source_directory(. DIR_SRCS)
#新增math子目錄
add_subdirectory(math)
#指定生成目標
add_executable(demo${DIR_SRCS})
# 新增連結庫
target_link_libraries(demoMathFunctions)

add_subdirectory命令指明本工程包含一個子目錄math,math目錄下的 CMakeLists.txt檔案和原始碼也會被處理 。target_link_libraries命令指明可執行檔案demo需要連線一個名為MathFunctions的連結庫 。
math子目錄的CMakeLists.txt檔案:

#查詢當前目錄下的所有原始檔
#並將名稱儲存到DIR_LIB_SRCS變數
aux_source_directory(. DIR_LIB_SRCS)
#生成連結庫
add_library(MathFunctions ${DIR_LIB_SRCS})

add_library命令將math目錄中的原始檔編譯為靜態連結庫。

六、自定義編譯選項

1、自定義編譯選項簡介

CMake允許為工程增加編譯選項,從而可以根據使用者的環境和需求選擇最合適的編譯方案。
例如,可以將MathFunctions庫設為一個可選的庫,如果該選項為ON ,就使用MathFunctions庫定義的數學函數來進行運算,否則就呼叫標準庫中的數學函數庫。

2、CMakeLists 檔案編寫

在根目錄的CMakeLists.txt檔案指定自定義編譯選項:

# CMake 最低版本號要求
cmake_minimum_required (VERSION 2.8)
# 專案資訊
project (demo)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
# 加入一個設定標頭檔案,用於處理 CMake 對原始碼的設定
configure_file (
  "${PROJECT_SOURCE_DIR}/config.h.in"
  "${PROJECT_BINARY_DIR}/config.h"
  )
# 是否使用自己的MathFunctions庫
option (USE_MYMATH
          "Use provided math implementation" ON)
# 是否加入 MathFunctions 庫
if (USE_MYMATH)
  include_directories ("${PROJECT_SOURCE_DIR}/math")
  add_subdirectory (math)
  set (EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif (USE_MYMATH)

# 查詢當前目錄下的所有原始檔
# 並將名稱儲存到 DIR_SRCS 變數
aux_source_directory(. DIR_SRCS)
# 指定生成目標
add_executable (demo${DIR_SRCS})
target_link_libraries (demo  ${EXTRA_LIBS})

configure_file命令用於加入一個設定標頭檔案config.h,config.h檔案由CMake從config.h.in生成,通過預定義一些引數和變數來控制程式碼的生成。
option命令新增了一個USE_MYMATH選項,並且預設值為ON。
根據USE_MYMATH變數的值來決定是否使用自己編寫的MathFunctions庫。

3、修改原始檔的呼叫

修改main.cpp檔案,讓其根據USE_MYMATH的預定義值來決定是否呼叫標準庫還是MathFunctions庫。

#include <cstdio>
#include <cstdlib>
#include <config.h>

#ifdef USE_MYMATH
#include <MathFunctions.h>
#else
#include <cmath>
#endif

int main(int argc, char *argv[])
{
    if (argc < 3)
    {
        printf("Usage: %s base exponent n", argv[0]);
        return 1;
    }

    double base = atof(argv[1]);
    int exponent = atoi(argv[2]);

#ifdef USE_MYMATH
    printf("Now we use our own Math library. n");
    double result = power(base, exponent);
#else
    printf("Now we use the standard library. n");
    double result = pow(base, exponent);
#endif

    printf("%g ^ %d is %gn", base, exponent, result);
    return 0;
}

4、編寫config.h.in檔案

main.cpp檔案包含了一個config.h檔案,config.h檔案預定義了USE_MYMATH 的值。但不會直接編寫config.h檔案,為了方便從CMakeLists.txt中匯入設定,通常編寫一個config.h.in檔案,內容如下:
#cmakedefine USE_MYMATH
CMake會自動根據CMakeLists.txt組態檔中的設定自動生成config.h檔案。

5、編譯工程

修改CMakeLists.txt檔案,USE_MYMATH為OFF,使用標準庫。

# 是否使用自己的MathFunctions庫
option (USE_MYMATH
          "Use provided math implementation" OFF)

在build目錄下cmake ..,make,執行程式:

七、安裝和測試

1、客製化安裝規則

在math/CMakeLists.txt檔案指定MathFunctions庫的安裝規則:

#指定MathFunctions庫的安裝路徑
install(TARGETS MathFunctions DESTINATION bin)
install(FILES MathFunctions.h DESTINATION include)

修改根目錄的CMakeLists.txt檔案指定目標檔案的安裝規則:

#指定安裝路徑
install(TARGETS test DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/config.h"
        DESTINATION include)

通過對安裝規則的客製化,生成的目標檔案和MathFunctions函數庫 libMathFunctions.o檔案將會被拷貝到/usr/local/bin中,而MathFunctions.h和生成的config.h檔案則會被複製到/usr/local/include中。
/usr/local是預設安裝到的根目錄,可以通過修改 CMAKE_INSTALL_PREFIX 變數的值來指定檔案應該拷貝到哪個根目錄。

2、為工程新增測試

CMake提供了一個CTest測試工具。在專案根目錄的CMakeLists.txt檔案中呼叫一系列的add_test 命令。

    #啟用測試
    enable_testing()
    #測試程式是否成功執行
    add_test(test_run demo 5 2)
    #測試幫助資訊是否可以正常提示
    add_test(test_usage demo)
    set_tests_properties(test_usage
      PROPERTIES PASS_REGULAR_EXPRESSION "Usage: .* base exponent")
    #測試5的平方
    add_test(test_5_2 demo 5 2)
    set_tests_properties(test_5_2
      PROPERTIES PASS_REGULAR_EXPRESSION "is 25")
    #測試10的5次方
    add_test(test_10_5 demo 10 5)
    set_tests_properties(test_10_5
      PROPERTIES PASS_REGULAR_EXPRESSION "is 100000")
    #測試2的10次方
    add_test(test_2_10 demo 2 10)
    set_tests_properties(test_2_10
      PROPERTIES PASS_REGULAR_EXPRESSION "is 1024")


第一個測試test_run用來測試程式是否成功執行並返回0值。剩下的三個測試分別用來測試 5 的 平方、10 的 5 次方、2 的 10 次方是否都能得到正確的結果。其中PASS_REGULAR_EXPRESSION用來測試輸出是否包含後面跟著的字串。
如果要測試更多的輸入資料,可以通過編寫宏來實現:

# 啟用測試
    enable_testing()

    # 測試程式是否成功執行
    add_test (test_run demo 5 2)

    # 測試幫助資訊是否可以正常提示
    add_test (test_usage demo)
    set_tests_properties (test_usage
      PROPERTIES PASS_REGULAR_EXPRESSION "Usage: .* base exponent")

    # 測試 5 的平方
    # add_test (test_5_2 Demo 5 2)

    # set_tests_properties (test_5_2
    #  PROPERTIES PASS_REGULAR_EXPRESSION "is 25")

    # 測試 10 的 5 次方
    # add_test (test_10_5 Demo 10 5)

    # set_tests_properties (test_10_5
    #  PROPERTIES PASS_REGULAR_EXPRESSION "is 100000")

    # 測試 2 的 10 次方
    # add_test (test_2_10 Demo 2 10)

    # set_tests_properties (test_2_10
    #  PROPERTIES PASS_REGULAR_EXPRESSION "is 1024")

    # 定義一個宏,用來簡化測試工作
    macro (do_test arg1 arg2 result)
      add_test (test_${arg1}_${arg2} demo ${arg1} ${arg2})
      set_tests_properties (test_${arg1}_${arg2}
        PROPERTIES PASS_REGULAR_EXPRESSION ${result})
    endmacro (do_test)

    # 利用 do_test 宏,測試一系列資料
    do_test (5 2 "is 25")
    do_test (10 5 "is 100000")
    do_test (2 10 "is 1024")

八、GDB支援

讓CMake支援gdb的設定只需要指定Debug模式下開啟-g選項:

set(CMAKE_BUILD_TYPE "Debug")
set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb")
set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")

生成的程式可以直接使用gdb來偵錯。

九、新增環境檢查

使用平台相關的特性時,需要對系統環境做檢查。檢查系統是否自帶pow函數,如果有pow函數,就使用;否則使用自定義的power函數。

1、新增 CheckFunctionExists 宏

首先在頂層CMakeLists.txt檔案中新增CheckFunctionExists.cmake 宏,並呼叫check_function_exists命令測試連結器是否能夠在連結階段找到 pow函數。

#檢查系統是否支援 pow 函數
include (${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)
check_function_exists (pow HAVE_POW)
check_function_exists需要放在configure_file命令前。

2、預定義相關宏變數

修改 config.h.in 檔案,預定義相關的宏變數。

// does the platform provide pow function?
#cmakedefine HAVE_POW

3、在程式碼中使用宏和函數

修改 main.cpp檔案 ,在程式碼中使用宏和函數。

#include <stdio.h>
#include <stdlib.h>
#include <config.h>

#ifdef HAVE_POW
#include <math.h>
#else
#include <MathFunctions.h>
#endif

int main(int argc, char *argv[])
{
    if (argc < 3)
    {
        printf("Usage: %s base exponent n", argv[0]);
        return 1;
    }
    double base = atof(argv[1]);
    int exponent = atoi(argv[2]);

#ifdef HAVE_POW
    printf("Now we use the standard library. n");
    double result = pow(base, exponent);
#else
    printf("Now we use our own Math library. n");
    double result = power(base, exponent);
#endif

    printf("%g ^ %d is %gn", base, exponent, result);
    return 0;
}

十、新增版本號

修改頂層CMakeLists.txt檔案,在project命令後分別指定當前的專案的主版本號和副版本號。

# CMake 最低版本號要求
cmake_minimum_required (VERSION 2.8)
# 專案資訊
project (demo)

set (Demo_VERSION_MAJOR 1)
set (Demo_VERSION_MINOR 0)

set(CMAKE_INCLUDE_CURRENT_DIR ON)

#檢查系統是否支援 pow 函數
include (${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)
check_function_exists (pow HAVE_POW)

# 加入一個設定標頭檔案,用於處理 CMake 對原始碼的設定
configure_file (
  "${PROJECT_SOURCE_DIR}/config.h.in"
  "${PROJECT_BINARY_DIR}/config.h"
  )

# 是否加入 MathFunctions 庫
if (NOT HAVE_POW)
  include_directories ("${PROJECT_SOURCE_DIR}/math")
  add_subdirectory (math)
  set (EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif (NOT HAVE_POW)

# 查詢當前目錄下的所有原始檔
# 並將名稱儲存到 DIR_SRCS 變數
aux_source_directory(. DIR_SRCS)
# 指定生成目標
add_executable (demo ${DIR_SRCS})
target_link_libraries (demo  ${EXTRA_LIBS})

#指定安裝路徑
install(TARGETS demo DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/config.h"
        DESTINATION include)

    # 啟用測試
    enable_testing()

    # 測試程式是否成功執行
    add_test (test_run demo 5 2)

    # 測試幫助資訊是否可以正常提示
    add_test (test_usage demo)
    set_tests_properties (test_usage
      PROPERTIES PASS_REGULAR_EXPRESSION "Usage: .* base exponent")

    # 測試 5 的平方
    # add_test (test_5_2 Demo 5 2)

    # set_tests_properties (test_5_2
    #  PROPERTIES PASS_REGULAR_EXPRESSION "is 25")

    # 測試 10 的 5 次方
    # add_test (test_10_5 Demo 10 5)

    # set_tests_properties (test_10_5
    #  PROPERTIES PASS_REGULAR_EXPRESSION "is 100000")

    # 測試 2 的 10 次方
    # add_test (test_2_10 Demo 2 10)

    # set_tests_properties (test_2_10
    #  PROPERTIES PASS_REGULAR_EXPRESSION "is 1024")

    # 定義一個宏,用來簡化測試工作
    macro (do_test arg1 arg2 result)
      add_test (test_${arg1}_${arg2} demo ${arg1} ${arg2})
      set_tests_properties (test_${arg1}_${arg2}
        PROPERTIES PASS_REGULAR_EXPRESSION ${result})
    endmacro (do_test)

    # 利用 do_test 宏,測試一系列資料
    do_test (5 2 "is 25")
    do_test (10 5 "is 100000")
    do_test (2 10 "is 1024")

分別指定當前的專案的主版本號和副版本號。
為了在程式碼中獲取版本資訊,可以修改 config.h.in 檔案,新增兩個預定義變數:

// the configured options and settings for Tutorial
#define Demo_VERSION_MAJOR @Demo_VERSION_MAJOR@
#define Demo_VERSION_MINOR @Demo_VERSION_MINOR@

// does the platform provide pow function?
#cmakedefine HAVE_POW

直接在原始碼中使用:

#include <stdio.h>
#include <stdlib.h>
#include <config.h>

#ifdef HAVE_POW
#include <math.h>
#else
#include <MathFunctions.h>
#endif

int main(int argc, char *argv[])
{
    if (argc < 3)
    {
        // print version info
        printf("%s Version %d.%dn",
              argv[0],
              Demo_VERSION_MAJOR,
              Demo_VERSION_MINOR);
        printf("Usage: %s base exponent n", argv[0]);
        return 1;
    }

    double base = atof(argv[1]);
    int exponent = atoi(argv[2]);

#ifdef HAVE_POW
    printf("Now we use the standard library. n");
    double result = pow(base, exponent);
#else
    printf("Now we use our own Math library. n");
    double result = power(base, exponent);
#endif

    printf("%g ^ %d is %gn", base, exponent, result);
    return 0;
}

十一、生成安裝包

1、增加CPack模組

CMake提供了一個專門用於打包的工具CPack,用於設定生成各種平台上的安裝包,包括二進位制安裝包和原始碼安裝包。
首先在頂層的CMakeLists.txt檔案尾部新增下面幾行:

# 構建一個 CPack 安裝包
include (InstallRequiredSystemLibraries)
set (CPACK_RESOURCE_FILE_LICENSE
  "${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set (CPACK_PACKAGE_VERSION_MAJOR "${Demo_VERSION_MAJOR}")
set (CPACK_PACKAGE_VERSION_MINOR "${Demo_VERSION_MINOR}")
include (CPack)

匯入InstallRequiredSystemLibraries模組,便於匯入CPack模組;
設定一些CPack相關變數,包括版權資訊和版本資訊
匯入CPack模組。
在頂層目錄下建立License.txt檔案內如如下:

The MIT License (MIT)

Copyright (c) 2018 Scorpio Studio

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

2、生成安裝包

生成二進位制安裝包:
cpack -C CPackConfig.cmake
生成原始碼安裝包:
cpack -C CPackSourceConfig.cmake
上述兩個命令都會在目錄下建立3個不同格式的二進位制包檔案:
demo-1.0.1-Linux.tar.gz
demo-1.0.1-Linux.tar.Z
demo-1.0.1-Linux.sh
3個二進位制包檔案所包含的內容是完全相同的。

Ubuntu 18.04下安裝最新CMake及CMake簡單使用  https://www.linuxidc.com/Linux/2018-09/154165.htm 

Linux公社的RSS地址:https://www.linuxidc.com/rssFeed.aspx

本文永久更新連結地址https://www.linuxidc.com/Linux/2018-09/154166.htm


IT145.com E-mail:sddin#qq.com