CMake从入门到放弃


一直都用cmake,但是也就是在GitHub上的项目写一点简单的东西,并没有正儿八经学过,因此抽空看了一点。主要从CMake官网的tutorial上学。

官网分为12个step,都在CMake源码的Help/guide/tutorial文件夹中可以找到。能看多少就看多少。

Step 1

首先最开始是最简单的CMakeLists.txt,只有三行

1
2
3
4
5
6
7
cmake_minimum_required(VERSION 3.10)

# set the project name
project(Tutorial)

# add the executable
add_executable(Tutorial tutorial.cxx)

分别指定了cmake的最低版本,项目的名称和生成的目标文件,就简单的执行:

1
2
3
mkdir build
cmake ..
make

在Unix下就可以生成可执行文件了。

然后接着要将版本号传递给可执行文件,并且随着版本号变化可以不用修改源码。

首先修改CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
cmake_minimum_required(VERSION 3.10)

# set the project name
project(Tutorial VERSION 1.3)

# add the executable
add_executable(Tutorial tutorial.cxx)

configure_file(TutorialConfig.h.in TutorialConfig.h)

target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
)

然后在CMakeLists.txt同级目录下编写TutorialConfig.h.in:

1
2
3
// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@

对于configure_file,cmake定义大概是这样的:将文件复制到另一个位置并修改其内容。当然,这里的修改其内容也不是任意地修改,也是遵循一定的规则:将input文件复制到output文件,并在输入文件内容中的变量,替换引用为@VAR@或${VAR}的变量值。每个变量引用将替换为该变量的当前值,如果未定义该变量,则为空字符串。

所以配置的时候就会在build文件夹下,生成TutorialConfig.h。如下:

1
2
3
// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR 1
#define Tutorial_VERSION_MINOR 3

就可以控制版本号了。而这个版本号是从CMakeLists.txt中获取,所以就不用修改源码,十分方便。

然后我又试了一下,发现可以指定configure_file的输出路径:

修改如下:

1
configure_file(TutorialConfig.h.in ${PROJECT_BINARY_DIR}/test/TutorialConfig.h)

执行:

1
2
mkdir build
cmake ..

可以看到build文件夹下有test文件夹,里吗有输出的头文件。

这个时候通过尝试我发现有两个方法:

要么修改CMakeLists.txt:

1
2
3
target_include_directories(Tutorial PUBLIC
${PROJECT_BINARY_DIR}/test
)

要么修改调用头文件:

1
2
3
#include "TutorialConfig.h"
// 修改为:
#include "test/TutorialConfig.h"

然后就是用C++ 11标准。

add_executable前面加入:

1
2
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

然后将代码换成C++ 11的标准,发现也可以用。

Step 2

在Step 1的基础上,加入了一个子目录MathFunctions。里面放了一个自己实现的mysqrt.cxx和对应的头文件MathFunctions.h

首先在子目录下加入CMakeLists.txt:

1
add_library(MathFunctions mysqrt.cxx)

这里的作用将mysqrt.cxx编译成libMathFunctions.a

然后在这个项目的CMakeLists.txt中写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# add the MathFunctions library
add_subdirectory(MathFunctions)

# add the executable
add_executable(Tutorial tutorial.cxx)

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"
)

首先第一行add_subdirectory就是去找到子目录。target_link_libraries则说明Tutorial要与库libMathFunctions链接。在这里target_link_libraries和上面的add_library的名字要相同,与子目录的名字不一定要相同。

target_include_directories则用于找到头文件。

然后在其中加入选项来控制是否用使用自己实现的函数。

在CMakeLists中进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# configure a header file to pass some of the CMake settings
# to the source code
configure_file(TutorialConfig.h.in TutorialConfig.h)

# add the MathFunctions library
if(USE_MYMATH)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions")
endif()

# add the executable
add_executable(Tutorial tutorial.cxx)

target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})

# 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}"
${EXTRA_INCLUDES}
)

首先在TutorialConfig.h.in中加入:

1
#cmakedefine USE_MYMATH

对源文件做一定修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// A simple program that computes the square root of a number
#include <cmath>
#include <iostream>
#include <string>

#include "TutorialConfig.h"

#ifdef USE_MYMATH
# include "MathFunctions.h"
#endif

int main(int argc, char* argv[])
{
if (argc < 2) {
// report version
std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "."
<< Tutorial_VERSION_MINOR << std::endl;
std::cout << "Usage: " << argv[0] << " number" << std::endl;
return 1;
}

// convert input to double
const double inputValue = std::stod(argv[1]);

// calculate square root
#ifdef USE_MYMATH
const double outputValue = mysqrt(inputValue);
#else
const double outputValue = sqrt(inputValue);
#endif
std::cout << "The square root of " << inputValue << " is " << outputValue
<< std::endl;
return 0;
}

注意:需要先包含头文件”TutorialConfig.h”。这样在cmake的时候可以用选项控制ifdef。最开始我把#include "TutorialConfig.h"放到了

1
2
3
#ifdef USE_MYMATH
# include "MathFunctions.h"
#endif

下面,一直显示# include "MathFunctions.h"无法显示。因为没有用cmakedefine进行定义。

最后就可以用

1
2
cmake .. -DUSE_MYMATH=ON
cmake .. -DUSE_MYMATH=OFF

来控制执行哪段代码。

Step 3

在这里将Step 2中CMakeLists.txt中的EXTRA_INCLUDES都删了,取代的方式是在MathFunctions中使用target_include_directories来让外部的函数调用。如下:

1
2
3
target_include_directories(MathFunctions
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
)

在这里用INTERFACE关键字,文档中解释说只给消费者使用不给生产者使用。这个关键词有很多文章解释,暂时不管。

Step 4

这一步是指定安装目录和做一些简单的测试。

要安装MathFunctions库,我们可以加入如下语句到MathFunctions/CMakeLists.txt中去:

1
2
install(TARGETS MathFunctions DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)

其中DESTINATION是相对路径。

而如果想要把Tutorial也安装到对应路径,可以用

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

放在根目录的CMakeLists.txt

这样子默认就会安装在根目录的/bin/lib/include

如果要安装到指定目录,有两种方法:

  • 在项目根目录到CMakeLists.txtProject选项下方,加上:

    1
    set(CMAKE_INSTALL_PREFIX /root/cmake-tutorial/install_path)
  • 在cmake配置的时候,用:

    1
    cmake -DCMAKE_INSTALL_PREFIX="/root/install" ..

    这两种方法都可以改变安装目录。

CMake的test

一般似乎是先启动测试:enable_testing(),然后用add_test()添加测试内容,然后最后用set_tests_properties()来设置测试的属性。

在原来的CMakelists.txt后加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
enable_testing()

# does the application run
add_test(NAME Runs COMMAND Tutorial 25)

# does the usage message work?
add_test(NAME Usage COMMAND Tutorial)
set_tests_properties(Usage
PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number"
)

# define a function to simplify adding tests
function(do_test target arg result)
add_test(NAME Comp${arg} COMMAND ${target} ${arg})
set_tests_properties(Comp${arg}
PROPERTIES PASS_REGULAR_EXPRESSION ${result}
)
endfunction()

# do a bunch of result based tests
do_test(Tutorial 4 "4 is 2")
do_test(Tutorial 9 "9 is 3")
do_test(Tutorial 5 "5 is 2.236")
do_test(Tutorial 7 "7 is 2.645")
do_test(Tutorial 25 "25 is 5")
do_test(Tutorial -25 "-25 is (-nan|nan|0)")
do_test(Tutorial 0.0001 "0.0001 is 0.01")

其中的PASS_REGULAR_EXPRESSION是正则匹配,后面的字符串预期会出现在输出中。

在编译好后用ctest命令即可看出测试效果,也可以用ctest -VVctest -N获取更多输出。

Step 5

Step 5是根据系统情况决定如何编译,在这里我觉得核心是

1
2
include(CheckSymbolExists)
check_symbol_exists(log "math.h" HAVE_LOG)

这个函数。

以上语句用于检测math.h中是否提供log函数。然后用一个if语句判断,如果提供的话,用target_compile_definitions或者add_definitions将宏传递给代码。

Step 6

在这里用一个程序生成了一个开方表,然后用add_custom_command来执行:

1
2
3
4
5
6
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
)

在这里首先编译得到MakeTable,然后在add_custom_command中指定了输出、命令和依赖。接下来就是把Table.h加入到库目录和include目录中去。

注意要用add_librarytarget_include_directories

1
2
3
4
5
6
7
8
9
10
11
add_library(MathFunctions
mysqrt.cxx
${CMAKE_CURRENT_BINARY_DIR}/Table.h
)

# state that anybody linking to us needs to include the current source dir
# to find MathFunctions.h, while we don't.
target_include_directories(MathFunctions
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
)

如果不用add_library则会出现Table.h无法生成,不将目录加入include的路径则会出现mysqrt.cxx无法去目标目录找到Table.h。