CMake - 讓人頭痛的止痛藥

CMake 編譯

MinGW + Sublime Text 簡(jiǎn)單編譯
CMake with Sublime Text

Why CMake?

先回答上面的問題:被逼的!這三個(gè)字是認(rèn)真的。

不管 CMake - Cross platform Make 是否是一個(gè)優(yōu)秀的構(gòu)建工具,不管你是否認(rèn)同 CMake,都無(wú)法否認(rèn) CMake 目前是 C++ 的 defacto build system。

參考代碼倉(cāng)庫(kù)示例:

Sublime + MinGW + CMake 打造自動(dòng)化 C++ 工程

CMake 是跨平臺(tái)編譯工具,比 make 更為高級(jí),通過編寫 CMakeLists.txt 文件,然后用 cmake 命令將其轉(zhuǎn)化為 make 所需要的 makefile 文件,最后用 make -G 命令生成指定編譯平臺(tái)的腳本或工程文件。

目前 CMake 已經(jīng)支持 Ninja、GCC 等編譯平臺(tái),同時(shí)也支持生成 Visual Studio、 Xcode、CodeBlocks、Sublime Text 等 IDE 的工程文件。支持 cmake 和 cmake-gui 兩種工作方式。

cmake path_to_cmakelists.txt -G "Sublime Text 2 - MinGW Makefiles"

目前已存在多種 Make 工具,GNU Make ,QT 的 qmake ,微軟的 nmake,BSD Make,Makepp 等等。這些 Make 工具遵循著不同的規(guī)范和標(biāo)準(zhǔn),所執(zhí)行的 Makefile 格式也千差萬(wàn)別。如果使用上面的 Make 工具,就得為每一種標(biāo)準(zhǔn)寫一次 Makefile,這將是一件讓人抓狂的工作。而 CMake 就是為了解決這種工作而開發(fā)出來(lái)讓人抓狂的工具!

cmake 命令提供了相關(guān)的文檔,可以使用命令打印到文件中。例如,以下命令會(huì)將所有 CMake 的模塊文檔保存到 cmake_modules.rst 文件中:

>cmake --help-modules cmake_modules.rst

reStructuredText 這種文件可以理解為是 Markdown 文件的精簡(jiǎn)版。

CMake 目前支持的編譯系統(tǒng):

  • AppleClang: Apple Clang for Xcode versions 4.4+.
  • Clang: Clang compiler versions 2.9+.
  • GNU: GNU compiler versions 4.4+.
  • MSVC: Microsoft Visual Studio versions 2010+.
  • SunPro: Oracle SolarisStudio versions 12.4+.
  • Intel: Intel compiler versions 12.1+.
  • NVIDIA CUDA: NVIDIA nvcc compiler 7.5+.

CMake 提供 5 個(gè)工具:

  • Command-Line Tools

    • cmake 生成編譯腳本
    • ctest 運(yùn)行測(cè)試如 ctest -R Qt -j8
    • cpack 打包安裝程序
  • Interactive Dialogs

    • cmake-gui 圖形界面的 cmake
    • ccmake CMake curses interface

在當(dāng)前目標(biāo)下執(zhí)行 cmake path_to_cmakelists_txt 命令,就會(huì)根據(jù)指定的列表文件生成編譯腳本,也可以直接在源代碼目錄中執(zhí)行這個(gè)命令,除非列表文件指定了禁止在源目錄生成。當(dāng)前目錄和指定的 CMakeLists.txt 所在的目錄是就 path-to-build 和 path-to-source 也對(duì)應(yīng) cmake-gui 兩個(gè)目錄。

CMake 強(qiáng)大的功能按以下類別進(jìn)行劃分,這也是主要的學(xué)習(xí)內(nèi)容:

命令分類 功能描述
cmake-buildsystem 構(gòu)建系統(tǒng),高邏輯級(jí)別上定義構(gòu)建目標(biāo),生成執(zhí)行文件、庫(kù)文件輸出等
cmake-commands 重點(diǎn)內(nèi)容,各種命令功能支持,分成 Scripting、Project、CTest 三類
cmake-compile-features 為各種編譯器提供參數(shù)或設(shè)置
cmake-developer CMake 擴(kuò)展開發(fā)支持,如編寫 Find Module 腳本
cmake-env-variables 環(huán)境變量讀寫支持
cmake-file-api 提供文件 API 訪問 <build>/.cmake/api/
cmake-generator-expressions 表達(dá)式生成器,在腳本運(yùn)行過程中生成個(gè)表達(dá)式的值
cmake-generators CMake 生成器,即 -G 指定生成的 Makefile 類型
cmake-language CMake 腳本語(yǔ)言
cmake-modules CMake 提供了一系列的模塊,如 FindPNG 找圖像庫(kù),還有 FindPHP4 等等
cmake-packages 依賴模塊功能支持,如查找依賴模塊 find_package
cmake-policies 考慮后向兼容,不同版本的 CMake 可按指定策略運(yùn)行編譯腳本
cmake-properties 編譯腳本屬性支持,如 INCLUDE_DIRECTORIES 屬性包含頭文件的目錄列表
cmake-qt CMake 提供 Qt 4 和 Qt 5 庫(kù)支持
cmake-server 棄用,使用 cmake-file-api 替代
cmake-toolchains 工具鏈接支持,如使用語(yǔ)言設(shè)置、平臺(tái)交叉編譯等
cmake-variables CMake 提供的變量支持非常豐富,內(nèi)置的變量按編譯工具、平臺(tái)等分成多類
cpack-generators 打包生成器,Archive、NSIS、NuGet、RPM、WIX 等等

以下是和當(dāng)前工程有關(guān)的變量:

<PROJECT-NAME>_BINARY_DIR
<PROJECT-NAME>_DESCRIPTION
<PROJECT-NAME>_HOMEPAGE_URL
<PROJECT-NAME>_SOURCE_DIR
<PROJECT-NAME>_VERSION
<PROJECT-NAME>_VERSION_MAJOR
<PROJECT-NAME>_VERSION_MINOR
<PROJECT-NAME>_VERSION_PATCH
<PROJECT-NAME>_VERSION_TWEAK
PROJECT_BINARY_DIR
PROJECT_DESCRIPTION
PROJECT_HOMEPAGE_URL
PROJECT_NAME
PROJECT_SOURCE_DIR
PROJECT_VERSION
PROJECT_VERSION_MAJOR
PROJECT_VERSION_MINOR
PROJECT_VERSION_PATCH
PROJECT_VERSION_TWEAK

因此 CMake 的編譯基本步驟如下:

  • 在當(dāng)前目錄為 cmake 配置 CMakeLists.txt;
  • 在當(dāng)前目錄執(zhí)行 cmake . 命令生成 make 使用的 makefile;
  • 執(zhí)行 make 進(jìn)行編譯;

如果編譯軟件使用了外部庫(kù),事先并不知道它的頭文件和鏈接庫(kù)的位置。CMake 使用 find_package 命令來(lái)查找它們的路徑,然后在編譯命令中加上包含它們的路徑:

FIND_PACKAGE( <name> [version] [EXACT] [QUIET] [NO_MODULE] [ [ REQUIRED | COMPONENTS ] [ componets... ] ] )

如:

FIND_PACKAGE( name REQUIRED)

CMake 解決項(xiàng)目的依賴時(shí),會(huì)自動(dòng)查找那些已知的軟件通常會(huì)保存的目錄路徑,如果找不到再通過依賴包的 Config-file 來(lái)查找。這條命令執(zhí)行后,CMake 會(huì)到變量 CMAKE_MODULE_PATH 指示的目錄中查找文件 Find<name>.cmake 并執(zhí)行,然后這個(gè)腳本返回 <name>affe_FOUND、<name>_INCLUDE_DIRS<name>_LIBRARIES 這些變量,如查找 Caffe:

find_package(Caffe REQUIRED)

if (NOT Caffe_FOUND)
    message(FATAL_ERROR "Caffe Not Found!")
endif (NOT Caffe_FOUND)

include_directories(${Caffe_INCLUDE_DIRS})

add_executable(demo ssd_detect.cpp)
target_link_libraries(demo ${Caffe_LIBRARIES})

首先明確一點(diǎn),cmake 本身不提供任何搜索庫(kù)的便捷方法,比如下面將要提到的 FindXXX.cmake 和 XXXConfig.cmake,庫(kù)的作者通常會(huì)提供這兩個(gè)文件,以方便使用者調(diào)用。

find_package 采用兩種模式搜索庫(kù):

  • Module 模式:搜索 CMAKE_MODULE_PATH 指定路徑下的 FindXXX.cmake 文件,執(zhí)行該文件,由它找到 XXX 庫(kù),并賦值給 XXX_INCLUDE_DIRS、XXX_LIBRARIES 兩個(gè)變量。

  • Config 模式:搜索 XXX_DIR 指定路徑下的 XXXConfig.cmake 文件,執(zhí)行該文件從而找到 XXX 庫(kù),并給 XXX_INCLUDE_DIRS、XXX_LIBRARIES 兩個(gè)變量賦值。

兩種模式看起來(lái)似乎差不多,不過 cmake 默認(rèn)采取 Module 模式,如果 Module 模式未找到庫(kù),才會(huì)采取 Config 模式。如果 XXX_DIR 路徑下找不到 XXXConfig.cmake 文件,則會(huì)找 /usr/local/lib/cmake/XXX/ 中的 XXXConfig.cmake 文件??傊?,Config 模式是一個(gè)備選策略。通常,庫(kù)安裝時(shí)會(huì)拷貝一份 XXXConfig.cmake 到系統(tǒng)目錄中,因此在沒有顯式指定搜索路徑時(shí)也可以順利找到。

以下是一個(gè) reStructuredText 格式展示的 Find Module 編寫格式示范,具體參考 cmake-developer 文檔:

# Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
# file Copyright.txt or https://cmake.org/licensing for details.

#[=======================================================================[.rst:
FindFoo
-------

Finds the Foo library.

Imported Targets
^^^^^^^^^^^^^^^^

This module provides the following imported targets, if found:

``Foo::Foo``
  The Foo library

Result Variables
^^^^^^^^^^^^^^^^

This will define the following variables:

``Foo_FOUND``
  True if the system has the Foo library.
``Foo_VERSION``
  The version of the Foo library which was found.
``Foo_INCLUDE_DIRS``
  Include directories needed to use Foo.
``Foo_LIBRARIES``
  Libraries needed to link to Foo.

Cache Variables
^^^^^^^^^^^^^^^

The following cache variables may also be set:

``Foo_INCLUDE_DIR``
  The directory containing ``foo.h``.
``Foo_LIBRARY``
  The path to the Foo library.

#]=======================================================================]

每個(gè)構(gòu)建腳本都奔構(gòu)建目標(biāo)來(lái)的,生成可執(zhí)行文件或是類庫(kù),如果是類庫(kù),那么可以指定靜態(tài) STATIC 或動(dòng)態(tài) SHARED 等:

add_library(archive archive.cpp zip.cpp lzma.cpp)
add_library(archive SHARED archive.cpp zip.cpp lzma.cpp)
add_library(archive STATIC archive.cpp zip.cpp lzma.cpp)
add_library(archive OBJECT archive.cpp zip.cpp lzma.cpp)

add_executable(zipapp zipapp.cpp)
target_link_libraries(zipapp archive)

生成共享庫(kù)的 add_library 命令格式如下:

add_library(libname [SHARED|STATIC|MODULE][EXCLUDE_FROM_ALL]source1 source2 ... sourceN)
  • SHARED 動(dòng)態(tài)庫(kù)(擴(kuò)展名為.so)
  • STATIC 靜態(tài)庫(kù)(擴(kuò)展名為.a)
  • MODULE 在使用 dyld 的系統(tǒng)有效,如果不支持 dyld,則被當(dāng)作 SHARED 對(duì)待。
  • EXCLUDE_FROM_ALL 參數(shù)的意思是這個(gè)庫(kù)不會(huì)被默認(rèn)構(gòu)建,除非有其他的組件依賴或者手工構(gòu)建。

CMake 會(huì)根據(jù)的生成庫(kù)的設(shè)置,為編譯鏈接程序提供和種鏈接方式:

set(CMAKE_EXE_LINKER_FLAGS "-static")

| 連接方式 | 連接選項(xiàng) | 優(yōu)缺點(diǎn) |
| 全靜態(tài) | -static -pthread -lrt -ldl | 生成的文件比較大,沒有運(yùn)行依賴。|
| 全動(dòng)態(tài) | -pthread -lrt -ldl | 生成文件最小,并且可能有依賴不兼容問題。 |
| 半靜態(tài) | -static-libgcc -L. -pthread -lrt -ldl | 靈活度大,結(jié)合了全靜態(tài)與全動(dòng)態(tài)兩種鏈接方式的優(yōu)點(diǎn)。 |

CMake 屬性和命令有些名字區(qū)別不大,通常用大小寫區(qū)別開來(lái),例如以下兩個(gè)屬性包含的是目錄列表:

  • INCLUDE_DIRECTORIES 包含頭文件目錄列表,供預(yù)處理程序搜索頭文件
  • LINK_DIRECTORIES 屬性包含目錄列表,包含鏈接階段使用的共享庫(kù)、模塊等

相關(guān)的命令 target_include_directories 為編譯的目標(biāo)提供頭文件目錄,指定的目標(biāo)必須已經(jīng)使用 add_executable()add_library() 定義:

target_include_directories(mylib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/mylib>
    $<INSTALL_INTERFACE:include/mylib>  # <prefix>/include/mylib
)

這個(gè)命令會(huì)將設(shè)置的目錄賦值給 INCLUDE_DIRECTORIES 屬性,也可以使用 set_property() 命令來(lái)設(shè)置屬性。

還有兩條和鏈接庫(kù)目錄有關(guān)的命令:

link_directories([AFTER|BEFORE] directory1 [directory2 ...])

target_link_directories(<target> [BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

兩者的差別就在于 target_link_directories 只為指定的編譯目標(biāo)提供鏈接庫(kù)目錄,供鏈接程序查找依賴文件。

以下類似的命令用于指定鏈接過程使用的依賴共享庫(kù)的鏈接:

link_libraries([item1 [item2 [...]]]
               [[debug|optimized|general] <item>] ...)
target_link_libraries(<target> ... <item>... ...)

類似地,target 前綴的命令表示只為指定的編譯目標(biāo)提供鏈接庫(kù),而且這個(gè)目標(biāo)要已經(jīng)使用 add_executable()add_library() 定義。

庫(kù)文件或可以執(zhí)行文件生成后就可以執(zhí)行安裝命令,將其拷貝到指定的位置:

install(TARGETS Tutorial DESTINATION bin)

按 CMake 教程,一般 CMakeList.txt 編寫流程:

# (Step 1) ==========================================
# A Basic Starting Point
# - Adding a Version Number and Configured Header File
# - Specify the C++ Standard

cmake_minimum_required( VERSION 2.8 )
project(Tutorial VERSION 1.0)

set(CMAKE_CXX_FLAGS "-std=c++11" )
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
?
# (Step 2) ==========================================
# Adding a Library

add_library(MathFunctions mysqrt.cxx)
# add the MathFunctions library
add_subdirectory(MathFunctions)

# add the executable
add_executable(Tutorial tutorial.cxx)

# (Step 3) ==========================================
# Adding Usage Requirements for Library

# target_compile_definitions()
# target_compile_options()

target_link_libraries(Tutorial PUBLIC MathFunctions)

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
target_include_directories(Tutorial PUBLIC
                          "${PROJECT_BINARY_DIR}"
                          "${PROJECT_SOURCE_DIR}/MathFunctions"
                          )

find_package(OpenCV REQUIRED)
# If the package has been found, several variables will
# be set, you can find the full list with descriptions
# in the OpenCVConfig.cmake file.
# Print some message showing some of them
message(STATUS "OpenCV library status:")
message(STATUS "    version: ${OpenCV_VERSION}")
message(STATUS "    libraries: ${OpenCV_LIBS}")
message(STATUS "    include path: ${OpenCV_INCLUDE_DIRS}")

# (Step 4) ==========================================
# Installing and Testing
# - Install Rules
# - Testing Support

install(TARGETS Tutorial DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
  DESTINATION include
  )

# (Step 5) ==========================================
# Adding System Introspection
# - Specify Compile Definition

# (Step 6) ==========================================
# Adding a Custom Command and Generated File

add_executable(MakeTable MakeTable.cxx)
add_custom_command(
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  DEPENDS MakeTable
  )

# (Step 7) ==========================================
# Building an Installer

include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set(CPACK_PACKAGE_VERSION_MAJOR "${Tutorial_VERSION_MAJOR}")
set(CPACK_PACKAGE_VERSION_MINOR "${Tutorial_VERSION_MINOR}")
include(CPack)

# (Step 10) ==========================================
# Adding Generator Expressions

add_library(tutorial_compiler_flags INTERFACE)
target_compile_features(tutorial_compiler_flags INTERFACE cxx_std_11)

set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")
target_compile_options(tutorial_compiler_flags INTERFACE
  "$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
  "$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)

# (Step 11) ==========================================
# Adding Export Configuration
        
install(TARGETS MathFunctions tutorial_compiler_flags
        DESTINATION lib
        EXPORT MathFunctionsTargets)
install(FILES MathFunctions.h DESTINATION include)

# (Step 12) ==========================================
# Packaging Debug and Release

set(CMAKE_DEBUG_POSTFIX d)
add_library(tutorial_compiler_flags INTERFACE)

add_executable(Tutorial tutorial.cxx)
set_target_properties(Tutorial PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX})

target_link_libraries(Tutorial PUBLIC MathFunctions)

set_property(TARGET MathFunctions PROPERTY VERSION "1.0.0")
set_property(TARGET MathFunctions PROPERTY SOVERSION "1")

Mixing Static and Shared

cmake_minimum_required(VERSION 3.10)

# set the project name and version
project(Tutorial VERSION 1.0)

# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# control where the static and shared libraries are built so that on windows
# we don't need to tinker with the path to run the executable
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")

option(BUILD_SHARED_LIBS "Build using shared libraries" ON)

# configure a header file to pass the version number only
configure_file(TutorialConfig.h.in TutorialConfig.h)

# add the MathFunctions library
add_subdirectory(MathFunctions)

# add the executable
add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial PUBLIC MathFunctions)

實(shí)際使用中,CMake 提供豐富的功能,列如:

include_directories(
    ${PROJECT_SOURCE_DIR}/../include/mq 
    ${PROJECT_SOURCE_DIR}/../include/incl 
    ${PROJECT_SOURCE_DIR}/../include/rapidjson
)
target_include_directories(${PROJECT_NAME} )

# 它相當(dāng)于 g++ -L 選項(xiàng)的作用,也相當(dāng)于環(huán)境變量中增加 LD_LIBRARY_PATH
link_directories(directory1 directory2 ...)
link_directories("/home/server/third/lib")

# 設(shè)定 SRC 變量,將源代碼路徑統(tǒng)一管理
set(SRC 
    ${PROJECT_SOURCE_DIR}/../include/incl/a.cpp 
    ${PROJECT_SOURCE_DIR}/../include/mq/b.cpp 
    ${PROJECT_SOURCE_DIR}/c.cpp
)
?
# 創(chuàng)建共享庫(kù)/靜態(tài)庫(kù)或引用庫(kù) add_library
?
# 設(shè)置生成共享庫(kù)的路徑
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib)
 
# 成的共享庫(kù)文件就叫做 lib[LIB_NAME].so
set(LIB_NAME freetype)

add_library(${LIB_NAME} SHARED ${SRC})
add_library(${LIB_NAME} STATIC ${SRC})

# 把 ${LIB_NAME} 庫(kù)和其它依賴的庫(kù)鏈接起來(lái)
# 以 libpthread.so 為例,這個(gè)命令相當(dāng) -lpthread
target_link_libraries(${LIB_NAME} pthread dl)
   
# 可執(zhí)行文件生成
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin)
add_executable(${PROJECT_NAME} ${SRC})
# 可執(zhí)行文件所依賴的庫(kù)
target_link_libraries(${PROJECT_NAME} pthread dl ${LIB_NAME})

訪問環(huán)境變量,讀取環(huán)境變量使用 $ENV{JAVA_HOME} 這樣的格式,寫入環(huán)境變量使用 set:

set( ENV{PATH} /home/martink )

if(NOT DEFINED ENV{JAVA_HOME})
    message(FATAL_ERROR "not defined environment variable:JAVA_HOME")  
endif()
#不能用if(ENV{JAVA_HOME})形式來(lái)判斷是否定義 
#但可以用if($ENV{JAVA_HOME})

總結(jié)一下,就可以看出來(lái),讀取環(huán)境變量時(shí)要在ENV前加符號(hào),而寫和if判斷是否定義時(shí),ENV{JAVA_HOME}指代變量名所以不加符號(hào)。

使用 C++ 11 標(biāo)準(zhǔn),可以通過不同方式設(shè)置:

# 設(shè)置C++標(biāo)準(zhǔn)為 C++ 11
set(CMAKE_CXX_STANDARD 11)

# 檢查c++編譯器標(biāo)志,設(shè)置c++11支持變量
include(CheckCXXCompilerFlag)
CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11)
CHECK_CXX_COMPILER_FLAG("-std=c++0x" COMPILER_SUPPORTS_CXX0X)

# 使用變量設(shè)置編譯標(biāo)志
if(COMPILER_SUPPORTS_CXX11)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
elseif(COMPILER_SUPPORTS_CXX0X)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")
else()
message(STATUS "The compiler ${CMAKE_CXX_COMPILER} has no C++11 support. Please use a different C++ compiler.")
endif()

CMake Ctest

Demo目錄結(jié)構(gòu)如下:

Test/
├── add.cpp
└── CMakeLists.txt

add.cpp

#include <iostream>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    if (argc != 3) {
        std::cout << "parameter error" << std::endl;
        return -1; 
    }   
 
    int a, b;
    a = atoi(argv[1]);
    b = atoi(argv[2]);
    std::cout << a << " + " << b << " is " << a + b << std::endl;
    return 0;
}

CMakeLists.txt

CMAKE_MINIMUM_REQUIRED(VERSION 3.3)
ADD_EXECUTABLE(add add.cpp)
enable_testing()
ADD_TEST(NAME test_add_2_3 COMMAND add 2 3)
SET_TESTS_PROPERTIES(test_add_2_3
    PROPERTIES PASS_REGULAR_EXPRESSION "5")
ADD_TEST(NAME test_add_4_5 COMMAND add 4 5)
SET_TESTS_PROPERTIES(test_add_4_5
    PROPERTIES PASS_REGULAR_EXPRESSION "9")

在 CMakeLists.txt 里面,我們添加了兩個(gè)測(cè)試用例。其中 PASS_REGULAR_EXPRESSION 用來(lái)測(cè)試輸出是否包含后面的字符串。

在 Test 目錄下建立 build 目錄:

cd build && cmake .., make, make test

像上面的方式寫測(cè)試用例還是比較繁瑣,還可以定義宏來(lái)簡(jiǎn)化:

CMAKE_MINIMUM_REQUIRED(VERSION 3.3)
ADD_EXECUTABLE(add add.cpp)
enable_testing()
 
macro(do_test ARG1 ARG2 RESULT)
    ADD_TEST(NAME test_add_${ARG1}_${ARG2} COMMAND add ${ARG1} ${ARG2})
    SET_TESTS_PROPERTIES(test_add_${ARG1}_${ARG2}
        PROPERTIES PASS_REGULAR_EXPRESSION ${RESULT})
endmacro(do_test)
do_test(2 3 5)
do_test(4 5 9)

配合 CPPUNIT 使用如下:

#include <cppunit/TestResult.h>
#include <cppunit/TestResultCollector.h>
#include <cppunit/TextOutputter.h>
#include <cppunit/TestRunner.h>
#include <cppunit/extensions/HelperMacros.h>
 
class StringTest : public CppUnit::TestFixture
{
    CPPUNIT_TEST_SUITE(StringTest);
    CPPUNIT_TEST(testSwap);
    CPPUNIT_TEST(testFind);
    CPPUNIT_TEST_SUITE_END();
public:
    void setUp()
    {   
        m_str1 = "Hello, world";
        m_str2 = "Hi, cppunit";
    }   
    void tearDown()
    {   
 
    }   
    void testSwap()
    {   
        std::string str1 = m_str1;
        std::string str2 = m_str2;
        m_str1.swap(m_str2);
        CPPUNIT_ASSERT(m_str1 == str2);
        CPPUNIT_ASSERT(m_str2 == str2);
    }   
    void testFind()
    {   
        int pos1 = m_str1.find(',');
        int pos2 = m_str2.rfind(',');
        CPPUNIT_ASSERT_EQUAL(5, pos1);
        CPPUNIT_ASSERT_EQUAL(2, pos2);
    }   
protected:
    std::string m_str1;
    std::string m_str2;
};
 
CPPUNIT_TEST_SUITE_REGISTRATION(StringTest);
int main(int argc, char *argv[])
{
    CppUnit::TestResult r;
    CppUnit::TestResultCollector rc;
    r.addListener(&rc);
 
    CppUnit::TestRunner runner;
    runner.addTest(CppUnit::TestFactoryRegistry::getRegistry().makeTest());
    runner.run(r);
 
    CppUnit::TextOutputter o(&rc, std::cout);
    o.write();
 
    return rc.wasSuccessful()?0:-1;
}

測(cè)試是軟件開發(fā)過程中極其重要的一環(huán),詳盡周密的測(cè)試能夠減少軟件BUG,提高軟件品質(zhì)。測(cè)試包括單元測(cè)試、系統(tǒng)測(cè)試等。其中單元測(cè)試是指針對(duì)軟件功能單元所作的測(cè)試,這里的功能單元可以是一個(gè)類的屬性或者方法,測(cè)試的目的是看這些基本單元是否工作正常。由于單元測(cè)試的內(nèi)容很基礎(chǔ),因此可以看作是測(cè)試工作的第一環(huán),該項(xiàng)工作一般由開發(fā)人員自行完成。如果條件允許,單元測(cè)試代碼的開發(fā)應(yīng)與程序代碼的開發(fā)同步進(jìn)行。

雖然不同程序的單元測(cè)試代碼不盡相同,但測(cè)試代碼的框架卻非常相似,于是便出現(xiàn)了一些單元測(cè)試類庫(kù),CppUnit便是其中之一。

CppUnit 是 XUnit 中的一員,XUnit 是一個(gè)大家族,還包括 JUnit 和 PythonUnit 等。CppUnit 簡(jiǎn)單實(shí)用,學(xué)習(xí)和使用起來(lái)都很方便,網(wǎng)上已有一些文章對(duì)其作介紹,但本文更著重于講解其中的基本概念和使用方法,以幫助初次接觸CppUnit的人員快速入門。

CMake OpenCV

使用 OpenCV 創(chuàng)建一個(gè)簡(jiǎn)單的程序 DisplayImage.cpp,如下所示。

#include <stdio.h>
#include <opencv2/opencv.hpp>

using namespace cv;

int main(int argc, char** argv )
{
    if ( argc != 2 )
    {
        printf("usage: DisplayImage.out <Image_Path>\n");
        return -1;
    }
    Mat image;
    image = imread( argv[1], 1 );
    if ( !image.data )
    {
        printf("No image data \n");
        return -1;
    }
    namedWindow("Display Image", WINDOW_AUTOSIZE );
    imshow("Display Image", image);
    waitKey(0);
    return 0;
}

為 CMake 命令創(chuàng)建一個(gè) CMakeLists.txt 文件:

cmake_minimum_required(VERSION 2.8)
project( DisplayImage )
# find_package( OpenCV REQUIRED )

include_directories(c:/download/OpenCV/opencv/build/include/)
link_directories(
    "c:/download/OpenCV/opencv/build/x64/vc15/lib/"
)

set(BUILD_SHARED_LIBS OFF)
set(OpenCV_LIBS 
    opencv_calib3d430
    opencv_core430
    opencv_dnn430
    opencv_features2d430
    opencv_flann430
    opencv_gapi430
    opencv_highgui430
    opencv_imgcodecs430
    opencv_imgproc430
    opencv_ml430
    opencv_objdetect430
    opencv_photo430
    opencv_python3
    opencv_stitching430
    opencv_ts430
    opencv_video430
    opencv_videoio430
    opencv_world430
    opencv_world430d
)
link_libraries( ${OpenCV_LIBS} )
add_executable( DisplayImage display.cpp )
target_link_libraries( DisplayImage ${OpenCV_LIBS} )

使用 CMake 生成可執(zhí)行文件:

cd <DisplayImage_directory>
cmake .
make

或者:

cmake --build .

在 Windows 平臺(tái)下和 MinGW 編譯器一起工作,指定生成 Makefile:

mkdir -p cmake-build && cd cmake-build
cmake .. -G"Unix Makefiles"

注意,不同編譯的器連接庫(kù)是沒辦法通過的,甚至同一套編譯器不同版本編譯出來(lái)的動(dòng)態(tài)鏈接庫(kù)也不能通用。所以要使用同版本的 MinGW 編譯出來(lái)鏈接庫(kù),除了使用 CMke 這個(gè)被逼著使用的東西,在 GCC 中可以選擇更通用的 GUN make。也可以像我一樣直接擼命令,以下是 Sublime 下使用的編譯配置文件,直接保存到 Preferences - Browser Packages - User 目錄下,命名就取 MinGW.sublime-build,sublime 會(huì)自動(dòng)讀取這個(gè)編譯配置文件,使用快捷鍵 Ctrl-B 就可以調(diào)出編譯命令:

{
    "env": {
        "PATH":"C:/MinGW-OpenCV-4.1.1-x64/x64/mingw/bin;%PATH%",
        "inc":"-IC:/MinGW-OpenCV-4.1.1-x64/include",
        "libpath":"-LC:/MinGW-OpenCV-4.1.1-x64/x64/mingw/lib",
        "libs":"-lopencv_calib3d411 -lopencv_core411 -lopencv_dnn411 -lopencv_features2d411 -lopencv_flann411 -lopencv_gapi411 -lopencv_highgui411 -lopencv_imgcodecs411 -lopencv_imgproc411 -lopencv_ml411 -lopencv_objdetect411 -lopencv_photo411 -lopencv_stitching411 -lopencv_video411 -lopencv_videoio411",
        "cc":"g++.exe -Wall -Wno-unused-variable"
    },
    "shell_cmd": "ECHO MinGW GCC 8.1.0 Compiling $file_name ... && %cc% -g -std=c++11 %inc% -c \"$file\" -o $file_base_name.o && g++.exe %libpath% -o ${file_base_name}.exe ${file_base_name}.o %libs% && echo done.",
    "file_regex": "^(...*?):([0-9]*):?([0-9]*)",
    "working_dir": "${file_path}",
    "selector": "source.c++",
    "encoding":"gbk",
    "quiet": true,
    "variants":[
        {
            "name":"(-std=c++17)",
            "shell_cmd":"ECHO Compiling (-std=c++17) $file_name ... && %cc% -g -std=c++17 -c \"$file\" -o $file_base_name.o && g++.exe -o ${file_base_name}.exe ${file_base_name}.o && ECHO Start run $file_name ... && ${file_base_name} "
        },
        {
            "name":"(-std=c++14)",
            "shell_cmd":"ECHO Compiling (-std=c++14) $file_name ... && %cc% -g -std=c++14 -c \"$file\" -o $file_base_name.o && g++.exe -o ${file_base_name}.exe ${file_base_name}.o && ECHO Start run $file_name ... && ${file_base_name} "
        },
        {
            "name":"(-std=c++11)",
            "shell_cmd":"ECHO Compiling (-std=c++11) $file_name ... && %cc% -g -std=c++11 -c \"$file\" -o $file_base_name.o && g++.exe -o ${file_base_name}.exe ${file_base_name}.o && ECHO Start run $file_name ... && ${file_base_name} "
        },
        {
            "name":"(-std=c++11) with ENV",
            "shell_cmd":"ECHO Compiling (-std=c++11) $file_name ... && %cc% -g -std=c++11 %inc% -c \"$file\" -o $file_base_name.o && g++.exe %libpath% -o ${file_base_name}.exe ${file_base_name}.o %libs% && ECHO Start run $file_name ... && ${file_base_name} "
        },
        {
            "name":"(-std=c++11) Release with ENV",
            "shell_cmd":"ECHO Compiling (-std=c++11) $file_name ... && %cc% -DNDEBUG -std=c++11 %inc% -c \"$file\" -o $file_base_name.o && g++.exe %libpath% -o ${file_base_name}.exe ${file_base_name}.o %libs% && ECHO Start run $file_name ... && ${file_base_name} "
        }
    }
}

配置中加入 PATH 的路徑是為了運(yùn)行編譯出來(lái)的程序能找到 OpenCV 的 DLL 文件。另外注意,GCC 中的 ld 鏈接程序默認(rèn)會(huì)自動(dòng)查找引用引用庫(kù)目錄中 .lib 擴(kuò)展名的文件。如果,編譯 OpenCV 生成的文件是 .dll.a 這樣古怪的名字,那么就找不到了。

在 Windows 和 Linux 系統(tǒng)上,程序的編譯鏈接都有動(dòng)態(tài)和靜態(tài)兩種方式,動(dòng)態(tài)鏈接 .dll 文件和 .so 文件是在程序執(zhí)行時(shí)使用的,而 .lib 引用庫(kù)文件是在程序編譯階段用來(lái)定位符號(hào)用的。如何是靜態(tài)鏈接,會(huì)使用到 .a 靜態(tài)鏈接庫(kù),靜態(tài)鏈接生成的程序文件運(yùn)行時(shí)就不需要依賴動(dòng)態(tài)鏈接庫(kù)了。

一般來(lái)說 Linux 中的庫(kù)文件名還可以這樣 libQt5Widgets.a 在引用時(shí)只需要取 Qt5Widgets 這部分,ld 查找的目錄順序是 /var/lib -> /usr/lib -> LD_LIBRARY_PATH 環(huán)境變量指定的目錄 -> 命令行指定的 -LPATH_TO_LIB 目錄。

如果遇到以下提示,請(qǐng)不要傻傻地去設(shè)置環(huán)境變量,這可以是因?yàn)?MinGW 使用的是 mingw32-make.exe 導(dǎo)致 CMake 檢測(cè)不到,復(fù)制一份改名 make.exe:

CMake Error: CMAKE_C_COMPILER not set, after EnableLanguage
CMake Error: CMAKE_CXX_COMPILER not set, after EnableLanguage

編譯前,還可以將 MinGW 編譯好的 OpenCV 的頭文件和庫(kù)文件放到對(duì)應(yīng)的位置:

C:\MinGW\x86_64-w64-mingw32\include

現(xiàn)在你應(yīng)該有一個(gè)可執(zhí)行文件,但它需要依賴 OpenCV 的動(dòng)態(tài)鏈接庫(kù),指定可以訪問到的一個(gè)路徑。運(yùn)行它給出一個(gè)圖像位置作為參數(shù),即:

set path=C:\OpenCV\build\bin
./DisplayImage lena.jpg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容