CMake 编译
- CMake Manual
- CMake Tutorial
- User Interaction Guide
- Using Dependencies Guide
- Ninja - small build system
Why CMake?
先回答上面的问题:被逼的!这三个字是认真的。
不管 CMake - Cross platform Make 是否是一个优秀的构建工具,不管你是否认同 CMake,都无法否认 CMake 目前是 C++ 的 defacto build system。
参考代码仓库示例:
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_DIRS
、XXX_LIBRARIES
两个变量。Config 模式:搜索 XXX_DIR 指定路径下的 XXXConfig.cmake 文件,执行该文件从而找到 XXX 库,并给
XXX_INCLUDE_DIRS
、XXX_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前加符号。
使用 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