Cmake

 

CMAKE介绍

CMAKE是一种跨平台的编译配置工具,它可以在不同的平台下基于同样的源代码文件生成对应的工程文件.例如Makefile和VS工程. 使用CMake的通用流程如下:

  1. 编写CMakeLists.txt
  2. 执行cmake PATH或ccmake PATH生成Makefile
  3. 使用make进行编译.

本文来源于另一位博主,重新敲一边,加深记忆.

代码网址在此.

案例一:单个源文件

现在我们有一个源文件:main.cc,它包含一个函数power,main中调用了它.

#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 %g\n", base, exponent, result);
    return 0;
}

下面在main.c同目录下编写CMakeLists.txt文件.

# CMake最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 项目信息
project (Demo1)
# 指定生成目标
add_executable(Demo main.cc)

CMakeLists.txt的语法较为简单,由命令 注释和空格组成,其中命令是不区分大小写的.符号#后面的内容被认为是注释.命令由命令名称 小括号和参数组成,参数之间使用空格进行间隔.

上面的CMakeLists.txt文件出现了一下命令:

  1. cmake_minimum_required:指定运行此配置文件所需的CMake的最低版本.
  2. project: 参数值Demo1,表示此项目的名称是Demo1.
  3. add_executable:将名为mian.cc的文件编译成一个名为Demo的可执行文件.

在当前目录执行cmake . cmake就会根据平台自动生成工程文件.

可以通过-g参数指定编译工具.

例如在windows下可以通过以下代码使用mingw进行编译和测试(-DCMAKE_BUILD_TYPE==Debug的作用是指定Debug模式):

mkdir build
cd build 
cmake .. -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Debug ..
mingw32-make
.\Demo1.exe 2 3

会得到输出:

2 ^ 3 is 8

cmake在windows下的默认工程类型是已安装的最新的vs,而且没有提供修改选项.

通过在系统目录添加一个名为cmake.bat内容如下的脚本,就不用每次调用的时候指定工程类型了.

@cmake.exe -G"MinGW Makefiles" %*

也可以在CMakeLists.txt文件中添加以下代码来指定DEBUG模式.

set(CMAKE_BUILD_TYPE on)

案例二 多文件 同目录

把power函数单独写进一个MathFunctions.c的源文件中,工程目录如下:

./Demo2
    |
    +--main.cc
    |
    +--MathFunctions.cc
    |
    +--MathFunctions.h

此时CMakeLists.txt修改如下:

# CMake最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 项目信息
project (Demo2)
# 指定生成目标
add_executable(Demo main.cc MathFunctions.cc)

唯一的改动是在add_executable命令中增加了一个MathFunctions.cc,如果源文件太多,可以使用aux_source_directory命令,该命令会查找指定目录下的所有源文件,然后把结果存进指定变量名.语法如下:

aux_source_directory( <dir> <variable>)

然后通过${variable}来使用变量. 因此可以修改CMakeLists.txt如下:

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)

# 项目信息
project (Demo2)

# 查找目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)

# 指定生成目标
add_executable(Demo ${DIR_SRCS})

案例三 多文件 多目录

现在将MathFunctions.h和MathFunctions.cc文件移动到单独的Math目录下

./Demo3
    |
    +--main.cc
    |
    +--math/
            |
            +--MathFunctions.cc
            |
            +--MathFunctions.h

对于这种情况,需要在根目录和math下各建立一个CMakeLists.txt文件,先将math目录下的文件编译成静态库再由main函数调用.

根目录中的CMakeLists.txt:

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)

# 项目信息
project (Demo3)

# 查找目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)

# 添加 math 子目录
add_subdirectory(math)

# 指定生成目标
add_executable(Demo ${DIR_SRCS})

# 添加链接库
target_link_libraries(Demo MathFunctions)

该文件第三行使用命令add_subdirectory指明项目包含一个子目录math,这样math目录下的CMakeList.txt 和源代码也会被处理. 第六行,使用命令 target_link_librarys指明可执行文件main需要链接一个名为MathFunctions的链接库.

子目录中的CMakeLists.txt:

# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_LIB_SRCS 变量
aux_source_directory(. DIR_LIB_SRCS)

# 指定生成 MathFunctions 链接库
add_library (MathFunctions ${DIR_LIB_SRCS})

在该文件中使用命令add_library将src目录中的源文件编译为静态链接库.

案例四 自定义编译选项

CMake允许为项目增加编译选项,从而根据用户的环境和需求选择最合适的编译方案.

例如,可以把MathFunctions库设为一个可选的库,如果该选项为ON,就是用该库的数学函数来进行计算.否则就调用标准库中的数学函数库.

我们要做的第一步是在顶层的CMakeLists.txt文件中添加该选项:

# CMake最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 项目信息
project(Demo4)
# 是否使用自己的MathFunctions库
option (USE_MYMATH
        "Use provided math implementation" ON)
# 加入一个配置头文件,用于处理CMake对源码的设置
configure_file(
    "${PROJECT_SOURCE_DIR}/config.h.in"
    "${}PROJECT_BINARY_DIR}/config.h"# 是否加入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})
  1. 其中第七行的configure_file命令用于加入一个配置文件config.h,这个文件由CMake从config.h.in生成,通过这样的机制,可以通过预定义一些参数和变量来控制代码的生成.
  2. 第13行的option命令添加了一个USE_MYMATH选项,并且默认值是ON
  3. 第17行根据USE_MYMATH变量的值决定是否使用math目录下的MathFunctions库.
  4. option选项必须放置在coufibure_file 前,否则会按照没有定义USE_MYMATH来生成config.h.

修改main.cc文件

之后修改main.cc文件,让其根据USE_MYMATH的预定义值来决定调用标准库还是MathFunctions库.

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

#ifdef USE_MYMATH
  #include <MathFunctions.h>
#else
  #include <math.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 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 %g\n", base, exponent, result);
    return 0;
}

另一种写法 上面的程序引用了一个config.h,config.h控制是否定义了USE_MYMATH.

我们创建一个config.h.in文件,内容如下:

#cmakedefine USE_MYMATH

CMake会自动根据CMakeLists配置文件中的设置生产config.h文件.

现在我们使用CMake进行编译即可得到使用MYMATH的编译版本.修改CMakeLists.txt中的USE_MYMATH为OFF即可使用库函数中的函数.

案例五 安装和测试

CMake也可以指定安装规则,以及添加测试.这两个功能分别可以通过在产生Makefile后使用make installmake test来执行.

在之前的GNU Makefile中,需要为此编写install和test两个伪目标和相应的规则,但在CMake中,这样的工作同样只需要简单的调用几条命令.

定制安装规则

首先在math/CMakeLists.txt的结尾添加下面两行:

# 指定 MathFunctions库的安装路径
install (TARGETS MathFunctions DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)

上面的代码指明了MathFunctions库的安装路径,之后修改根目录的CMakeLists文件,在末尾添加下面几行:

# 指定安装路径
install (TARGETS Demo DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/config.h"
        DESTINATION include)

通过上面的设置,生成的Demo文件和MathFunctions函数库libMathFunctions.o将会被复制到/usr/local/bin中,而MathFunctions.h和生成的config.h文件将会被复制到/usr/local/include中.

在windows下,默认安装目录是C:\Program Files (x86)\Demo5,Demo5是项目名

这里的/usr/local是默认安装目录,可以通过修改CMAKE_INSTALL_PREFIX变量的值来制定安装目录.

为工程添加测试

添加测试也很简单. CMake提供了一个称为CTest的测试工具.我们要做的只是在项目根目录的CMakeLists.txt文件中调用一系列的add_test命令.

# 启用测试
enable_testing()

#测试程序是否成功运行
add_test (test_run Demo 5 2)

# 测试帮助信息是否可以正常提示 PASS_REGULAR_EXPRESSION的作用是判断程序输出是否包含后面的字符串
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用来测试输出是否包含后面跟着的字符串.

测试结果如下:

PS D:\cmake_project\Demo5\build> make test
Running tests...
Test project D:/cmake_project/Demo5/build
    Start 1: test_run
1/5 Test #1: test_run .........................   Passed    0.01 sec
    Start 2: test_usage
2/5 Test #2: test_usage .......................   Passed    0.01 sec
    Start 3: test_5_2
3/5 Test #3: test_5_2 .........................   Passed    0.01 sec
    Start 4: test_10_5
4/5 Test #4: test_10_5 ........................   Passed    0.01 sec
    Start 5: test_2_10
5/5 Test #5: test_2_10 ........................   Passed    0.01 sec

100% tests passed, 0 tests failed out of 5

Total Test time (real) =   0.08 sec

如果有大量测试,可以通过编写宏来简化:

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(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函数.如果有,就使用它;否则使用我们自己定义的power函数.

添加CheckFunctionExists宏

首先在定层CMakeLists文件的configure_file前添加CheckFunctionExists.cmake宏,并调用check_function_exists命令测试链接器是否能在链接阶段找到pow函数.

在CMakeLists.txt中可以通过if(HAVE_POW)和if(NOT HAVE_POW)来判断系统是否提供了pow函数.在源代码中可以通过config.h中是否define HAVE_POW来判断系统是否提供了pow函数.

# 检查系统是否支持pow函数
include (${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)
check_function_exists (pow HAVE_POW)

预定义相关宏变量

接下来修改configure.h.in,预定义相关的宏变量.

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

在代码中使用宏和函数

最后一步是修改main.cc,在代码中使用宏和函数:

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

添加版本号

给项目添加和维护版本号是一个好习惯,这样有利于用户了解每个版本的维护情况,并及时了解当前多用的版本是否过时,或是否可能出现不兼容的情况.

首先修改顶层CMakeLists文件,在project命令后添加如下两行:

set (Demo_VERSION_MAJOR 1)
set (Demo_VERSION_MINOR 0)

分别指定当前项目的主版本号和副版本号.

之后,为了在代码中获取项目信息,我们可以修改config.h.in文件,添加两个预定义变量:

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

#define Demo_VERSION_MINOR @Demo_VERSION_MINOR@

这样就可以直接在代码中打印版本信息了:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "config.h"
#include "math/MathFunctions.h"
int main(int argc, char *argv[])
{
    if (argc < 3){
        // print version info
        printf("%s Version %d.%d\n",
            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]);
    
#if defined (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 %g\n", base, exponent, result);
    return 0;
}

生成安装包

本节将学习如何配置生成各种平台上的安装包,包括二进制安装包和源码安装包.为了完成这个任务,我们需要用到CPack,它是CMake提供的一个工具,专门用于打包.

首先在顶层的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)

上面的代码做了一下几个工作:

  1. 导入InstallRequeredSystemLibraries模块,以便之后导入CPack模块;
  2. 设置一些CPack相关变量,包括版权信息和版本信息,其中版本信息用了上一节定义的版本号;
  3. 导入CPack模块. 接下来的工作是像往常一样构建工程,并执行cpack命令.
    • 生成二进制安装包
      cpack -C CPackConfig.cmake
      
    • 生成源代码安装包
      cpack -C CPackSourceConfig.cmake
      

      我们可以试一下,执行cpack -C CPackConfig.cmake命令:

      PS D:\cmake_project\Demo8\build> cpack -C .\CPackConfig.cmake
      CPack: Create package using NSIS
      CPack: Install projects
      CPack: - Run preinstall target for: Demo8
      CPack: - Install project: Demo8
      CPack: Create package
      CPack: - package: D:/cmake_project/Demo8/build/Demo8-1.0.1-win32.exe generated.
      

      然后就可以使用Demo8-1.0.1-win32.exe文件来进行安装了,效果类似于make install,但是即使设置了CMAKE_INSTALL_PREFIX,安装目录也是C:\Program Files (x86)/project_name.

导入第三方库

在CMake中导入第三方库可以通过include_directorieslink_directories设置第三方库的路径,然后使用target_link_libraries进行链接.

在尝试对OpenCV进行编译的时候,笔者发现OpenCV自带了一个OpenCVConfig.CMAKE文件,只要在CMakeLists.txt中include这个文件,就能直接使用已经设置好的CMAKE变量.

project (yinxie_opencv)
cmake_minimum_required(VERSION 3.8)

SET (CMAKE_BUILD_TYPE "Debug")
SET (CMAKE_INSTALL_PREFIX  ${CMAKE_SOURCE_DIR})

include( "C:/Users/cat/Documents/code/opencv/build_mingw_debug/OpenCVConfig.cmake" )         # 直接引入 cmake 文件
# 在opencv的bin目录中有OpenCVConfig.cmake文件,也可以使用 find_package(OpenCV REQUIRED) 代替本行

message( STATUS "OpenCV library status:" )              # 输出一下得到的变量
message( STATUS "    version: ${OpenCV_VERSION}" )
message( STATUS "    libraries: ${OpenCV_LIBS}" )
message( STATUS "    include path: ${OpenCV_INCLUDE_DIRS}" )


include_directories( ${OpenCV_INCLUDE_DIRS} ) 
add_executable(Demo main.cpp)
target_link_libraries( Demo ${OpenCV_LIBS} ) # exe 链接 OpenCV

install(TARGETS Demo DESTINATION .)

链接本地库文件

link_directories并不推荐,官方推荐使用find_library获得库绝对目录然后接target_link_libraries.

find_library(VI_LIB vi ./vi/lib)
target_link_libraries(${PROJECT_NAME} ${VI_LIB})

将其他平台的项目迁移到CMake

CMake可以很轻松的构建出在适合各个平台执行的工程环境,如果当前的工程环境不是CMAKE,而是其它平台,也是有可能一直成功的.下面是几个常用平台的迁移方案.

autotools

使用CMake编译QT项目

使用mingw编译QT后就可以像opencv一样加入到CMakeLists.txt文件中.

实例如下:

set(CMAKE_INCLUDE_CURRENT_DIR ON)#qt建议包含当前目录
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)#这两个是qt的自动工具

find_package(Qt5 COMPONENTS Core Quick Widgets Gui PrintSupport REQUIRED)#去寻找qt库的cmake配置文件,可以根据需要添加对应的模块
#无需 include_directories()
FILE(GLOB FORMS "form/*.ui")
QT5_WRAP_UI(FORMS_UIC ${FORMS})#等价于 uic -o ui_*.h *.h
FILE(GLOB MOCS "inc/*.h")
QT5_WRAP_CPP(HEADERS_MOC ${MOCS})
FILE(GLOB RES "qrc/*.qrc")
QT5_ADD_RESOURCES(RES_RCC ${RES})
aux_source_directory(. DIR_SRCS)

add_executable(${PROJECT_NAME} ${DIR_SRCS} ${FORMS_UIC} ${HEADERS_MOC} ${RES_RCC})

target_link_libraries(
    ${PROJECT_NAME} 
    Qt5::Core 
    Qt5::Quick 
    Qt5::Widgets 
    Qt5::Gui
    Qt5::PrintSupport
    ${OpenCV_LIBS})

#如果没有下面的代码 QT程序在某些情况下会默认生成一个命令行窗口
# generate proper GUI program on specified platform
# if(WIN32) # Check if we are on Windows
# 	if(MSVC) # Check if we are using the Visual Studio compiler
# 		set_target_properties(${PROJECT_NAME} PROPERTIES
# 			WIN32_EXECUTABLE YES
# 			LINK_FLAGS "/ENTRY:mainCRTStartup"
# 		)
# 	elseif(CMAKE_COMPILER_IS_GNUCXX)
# 		SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mwindows") # Not tested
# 	else()
# 		message(SEND_ERROR "You are using an unsupported Windows compiler! (Not MSVC or GCC)")
# 	endif(MSVC)
# elseif(APPLE)
# 	set_target_properties(${PROJECT_NAME} PROPERTIES
# 			MACOSX_BUNDLE YES
# 	)
# elseif(UNIX)
# 	# Nothing special required
# else()
# 	message(SEND_ERROR "You are on an unsupported platform! (Not Win32, Mac OS X or Unix)")
# endif(WIN32)