CMake - 让人头痛的止痛药

CMake 编译

MinGW + Sublime Text 简单编译
CMake with Sublime Text

Why CMake?

先回答上面的问题:被逼的!这三个字是认真的。

不管 CMake - Cross platform Make 是否是一个优秀的构建工具,不管你是否认同 CMake,都无法否认 CMake 目前是 C++ 的 defacto build system。

参考代码仓库示例:

Sublime + MinGW + CMake 打造自动化 C++ 工程

CMake 是跨平台编译工具,比 make 更为高级,通过编写 CMakeLists.txt 文件,然后用 cmake 命令将其转化为 make 所需要的 makefile 文件,最后用 make -G 命令生成指定编译平台的脚本或工程文件。

目前 CMake 已经支持 Ninja、GCC 等编译平台,同时也支持生成 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 工具遵循着不同的规范和标准,所执行的 Makefile 格式也千差万别。如果使用上面的 Make 工具,就得为每一种标准写一次 Makefile,这将是一件让人抓狂的工作。而 CMake 就是为了解决这种工作而开发出来让人抓狂的工具!

cmake 命令提供了相关的文档,可以使用命令打印到文件中。例如,以下命令会将所有 CMake 的模块文档保存到 cmake_modules.rst 文件中:

>cmake --help-modules cmake_modules.rst

reStructuredText 这种文件可以理解为是 Markdown 文件的精简版。

CMake 目前支持的编译系统:

  • 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 个工具:

  • Command-Line Tools

    • cmake 生成编译脚本
    • ctest 运行测试如 ctest -R Qt -j8
    • cpack 打包安装程序
  • Interactive Dialogs

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

在当前目标下执行 cmake path_to_cmakelists_txt 命令,就会根据指定的列表文件生成编译脚本,也可以直接在源代码目录中执行这个命令,除非列表文件指定了禁止在源目录生成。当前目录和指定的 CMakeLists.txt 所在的目录是就 path-to-build 和 path-to-source 也对应 cmake-gui 两个目录。

CMake 强大的功能按以下类别进行划分,这也是主要的学习内容:

命令分类 功能描述
cmake-buildsystem 构建系统,高逻辑级别上定义构建目标,生成执行文件、库文件输出等
cmake-commands 重点内容,各种命令功能支持,分成 Scripting、Project、CTest 三类
cmake-compile-features 为各种编译器提供参数或设置
cmake-developer CMake 扩展开发支持,如编写 Find Module 脚本
cmake-env-variables 环境变量读写支持
cmake-file-api 提供文件 API 访问 <build>/.cmake/api/
cmake-generator-expressions 表达式生成器,在脚本运行过程中生成个表达式的值
cmake-generators CMake 生成器,即 -G 指定生成的 Makefile 类型
cmake-language CMake 脚本语言
cmake-modules CMake 提供了一系列的模块,如 FindPNG 找图像库,还有 FindPHP4 等等
cmake-packages 依赖模块功能支持,如查找依赖模块 find_package
cmake-policies 考虑后向兼容,不同版本的 CMake 可按指定策略运行编译脚本
cmake-properties 编译脚本属性支持,如 INCLUDE_DIRECTORIES 属性包含头文件的目录列表
cmake-qt CMake 提供 Qt 4 和 Qt 5 库支持
cmake-server 弃用,使用 cmake-file-api 替代
cmake-toolchains 工具链接支持,如使用语言设置、平台交叉编译等
cmake-variables CMake 提供的变量支持非常丰富,内置的变量按编译工具、平台等分成多类
cpack-generators 打包生成器,Archive、NSIS、NuGet、RPM、WIX 等等

以下是和当前工程有关的变量:

<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 的编译基本步骤如下:

  • 在当前目录为 cmake 配置 CMakeLists.txt;
  • 在当前目录执行 cmake . 命令生成 make 使用的 makefile;
  • 执行 make 进行编译;

如果编译软件使用了外部库,事先并不知道它的头文件和链接库的位置。CMake 使用 find_package 命令来查找它们的路径,然后在编译命令中加上包含它们的路径:

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

如:

FIND_PACKAGE( name REQUIRED)

CMake 解决项目的依赖时,会自动查找那些已知的软件通常会保存的目录路径,如果找不到再通过依赖包的 Config-file 来查找。这条命令执行后,CMake 会到变量 CMAKE_MODULE_PATH 指示的目录中查找文件 Find<name>.cmake 并执行,然后这个脚本返回 <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})

首先明确一点,cmake 本身不提供任何搜索库的便捷方法,比如下面将要提到的 FindXXX.cmake 和 XXXConfig.cmake,库的作者通常会提供这两个文件,以方便使用者调用。

find_package 采用两种模式搜索库:

  • Module 模式:搜索 CMAKE_MODULE_PATH 指定路径下的 FindXXX.cmake 文件,执行该文件,由它找到 XXX 库,并赋值给 XXX_INCLUDE_DIRSXXX_LIBRARIES 两个变量。

  • Config 模式:搜索 XXX_DIR 指定路径下的 XXXConfig.cmake 文件,执行该文件从而找到 XXX 库,并给 XXX_INCLUDE_DIRSXXX_LIBRARIES 两个变量赋值。

两种模式看起来似乎差不多,不过 cmake 默认采取 Module 模式,如果 Module 模式未找到库,才会采取 Config 模式。如果 XXX_DIR 路径下找不到 XXXConfig.cmake 文件,则会找 /usr/local/lib/cmake/XXX/ 中的 XXXConfig.cmake 文件。总之,Config 模式是一个备选策略。通常,库安装时会拷贝一份 XXXConfig.cmake 到系统目录中,因此在没有显式指定搜索路径时也可以顺利找到。

以下是一个 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.

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

每个构建脚本都奔构建目标来的,生成可执行文件或是类库,如果是类库,那么可以指定静态 STATIC 或动态 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)

生成共享库的 add_library 命令格式如下:

add_library(libname [SHARED|STATIC|MODULE][EXCLUDE_FROM_ALL]source1 source2 ... sourceN)
  • SHARED 动态库(扩展名为.so)
  • STATIC 静态库(扩展名为.a)
  • MODULE 在使用 dyld 的系统有效,如果不支持 dyld,则被当作 SHARED 对待。
  • EXCLUDE_FROM_ALL 参数的意思是这个库不会被默认构建,除非有其他的组件依赖或者手工构建。

CMake 会根据的生成库的设置,为编译链接程序提供和种链接方式:

set(CMAKE_EXE_LINKER_FLAGS "-static")

| 连接方式 | 连接选项 | 优缺点 |
| 全静态 | -static -pthread -lrt -ldl | 生成的文件比较大,没有运行依赖。|
| 全动态 | -pthread -lrt -ldl | 生成文件最小,并且可能有依赖不兼容问题。 |
| 半静态 | -static-libgcc -L. -pthread -lrt -ldl | 灵活度大,结合了全静态与全动态两种链接方式的优点。 |

CMake 属性和命令有些名字区别不大,通常用大小写区别开来,例如以下两个属性包含的是目录列表:

  • INCLUDE_DIRECTORIES 包含头文件目录列表,供预处理程序搜索头文件
  • LINK_DIRECTORIES 属性包含目录列表,包含链接阶段使用的共享库、模块等

相关的命令 target_include_directories 为编译的目标提供头文件目录,指定的目标必须已经使用 add_executable()add_library() 定义:

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

这个命令会将设置的目录赋值给 INCLUDE_DIRECTORIES 属性,也可以使用 set_property() 命令来设置属性。

还有两条和链接库目录有关的命令:

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

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

两者的差别就在于 target_link_directories 只为指定的编译目标提供链接库目录,供链接程序查找依赖文件。

以下类似的命令用于指定链接过程使用的依赖共享库的链接:

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

类似地,target 前缀的命令表示只为指定的编译目标提供链接库,而且这个目标要已经使用 add_executable()add_library() 定义。

库文件或可以执行文件生成后就可以执行安装命令,将其拷贝到指定的位置:

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)

实际使用中,CMake 提供丰富的功能,列如:

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

# 它相当于 g++ -L 选项的作用,也相当于环境变量中增加 LD_LIBRARY_PATH
link_directories(directory1 directory2 ...)
link_directories("/home/server/third/lib")

# 设定 SRC 变量,将源代码路径统一管理
set(SRC 
    ${PROJECT_SOURCE_DIR}/../include/incl/a.cpp 
    ${PROJECT_SOURCE_DIR}/../include/mq/b.cpp 
    ${PROJECT_SOURCE_DIR}/c.cpp
)
​
# 创建共享库/静态库或引用库 add_library
​
# 设置生成共享库的路径
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib)
 
# 成的共享库文件就叫做 lib[LIB_NAME].so
set(LIB_NAME freetype)

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

# 把 ${LIB_NAME} 库和其它依赖的库链接起来
# 以 libpthread.so 为例,这个命令相当 -lpthread
target_link_libraries(${LIB_NAME} pthread dl)
   
# 可执行文件生成
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin)
add_executable(${PROJECT_NAME} ${SRC})
# 可执行文件所依赖的库
target_link_libraries(${PROJECT_NAME} pthread dl ${LIB_NAME})

访问环境变量,读取环境变量使用 $ENV{JAVA_HOME} 这样的格式,写入环境变量使用 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})形式来判断是否定义 
#但可以用if($ENV{JAVA_HOME})

总结一下,就可以看出来,读取环境变量时要在ENV前加符号,而写和if判断是否定义时,ENV{JAVA_HOME}指代变量名所以不加符号。

使用 C++ 11 标准,可以通过不同方式设置:

# 设置C++标准为 C++ 11
set(CMAKE_CXX_STANDARD 11)

# 检查c++编译器标志,设置c++11支持变量
include(CheckCXXCompilerFlag)
CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11)
CHECK_CXX_COMPILER_FLAG("-std=c++0x" COMPILER_SUPPORTS_CXX0X)

# 使用变量设置编译标志
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目录结构如下:

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 里面,我们添加了两个测试用例。其中 PASS_REGULAR_EXPRESSION 用来测试输出是否包含后面的字符串。

在 Test 目录下建立 build 目录:

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

像上面的方式写测试用例还是比较繁琐,还可以定义宏来简化:

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;
}

测试是软件开发过程中极其重要的一环,详尽周密的测试能够减少软件BUG,提高软件品质。测试包括单元测试、系统测试等。其中单元测试是指针对软件功能单元所作的测试,这里的功能单元可以是一个类的属性或者方法,测试的目的是看这些基本单元是否工作正常。由于单元测试的内容很基础,因此可以看作是测试工作的第一环,该项工作一般由开发人员自行完成。如果条件允许,单元测试代码的开发应与程序代码的开发同步进行。

虽然不同程序的单元测试代码不尽相同,但测试代码的框架却非常相似,于是便出现了一些单元测试类库,CppUnit便是其中之一。

CppUnit 是 XUnit 中的一员,XUnit 是一个大家族,还包括 JUnit 和 PythonUnit 等。CppUnit 简单实用,学习和使用起来都很方便,网上已有一些文章对其作介绍,但本文更着重于讲解其中的基本概念和使用方法,以帮助初次接触CppUnit的人员快速入门。

CMake OpenCV

使用 OpenCV 创建一个简单的程序 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 命令创建一个 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 生成可执行文件:

cd <DisplayImage_directory>
cmake .
make

或者:

cmake --build .

在 Windows 平台下和 MinGW 编译器一起工作,指定生成 Makefile:

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

注意,不同编译的器连接库是没办法通过的,甚至同一套编译器不同版本编译出来的动态链接库也不能通用。所以要使用同版本的 MinGW 编译出来链接库,除了使用 CMke 这个被逼着使用的东西,在 GCC 中可以选择更通用的 GUN make。也可以像我一样直接撸命令,以下是 Sublime 下使用的编译配置文件,直接保存到 Preferences - Browser Packages - User 目录下,命名就取 MinGW.sublime-build,sublime 会自动读取这个编译配置文件,使用快捷键 Ctrl-B 就可以调出编译命令:

{
    "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 的路径是为了运行编译出来的程序能找到 OpenCV 的 DLL 文件。另外注意,GCC 中的 ld 链接程序默认会自动查找引用引用库目录中 .lib 扩展名的文件。如果,编译 OpenCV 生成的文件是 .dll.a 这样古怪的名字,那么就找不到了。

在 Windows 和 Linux 系统上,程序的编译链接都有动态和静态两种方式,动态链接 .dll 文件和 .so 文件是在程序执行时使用的,而 .lib 引用库文件是在程序编译阶段用来定位符号用的。如何是静态链接,会使用到 .a 静态链接库,静态链接生成的程序文件运行时就不需要依赖动态链接库了。

一般来说 Linux 中的库文件名还可以这样 libQt5Widgets.a 在引用时只需要取 Qt5Widgets 这部分,ld 查找的目录顺序是 /var/lib -> /usr/lib -> LD_LIBRARY_PATH 环境变量指定的目录 -> 命令行指定的 -LPATH_TO_LIB 目录。

如果遇到以下提示,请不要傻傻地去设置环境变量,这可以是因为 MinGW 使用的是 mingw32-make.exe 导致 CMake 检测不到,复制一份改名 make.exe:

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

编译前,还可以将 MinGW 编译好的 OpenCV 的头文件和库文件放到对应的位置:

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

现在你应该有一个可执行文件,但它需要依赖 OpenCV 的动态链接库,指定可以访问到的一个路径。运行它给出一个图像位置作为参数,即:

set path=C:\OpenCV\build\bin
./DisplayImage lena.jpg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342