关于cmake

​https://cmake.org/cmake/help/latest/​

CMake是一个管理源代码构建的工具,它先生成makefile然后再调用原生的编译系统编译和连接应用程序。

cmake广泛使用于c和c++,它也可以用于其它语言。

关于安装cmake

下载地址:​​https://cmake.org/download/#latest​

在linux(ubbuntu)上:

sudo apt install cmake

安装之后查看版本

cmake --version

windows下是傻瓜式的安装,一直下一步即可~

教程

​https://cmake.org/cmake/help/latest/guide/tutorial/index.html​

教程就像是编程界的”helloworld“一样,通过实际的例子走完整个工作流程,从而了解cmake的基础用法。

关于示例代码:

​https://cmake.org/cmake/help/latest/guide/tutorial/index.html​

cmake(1) -- 基础教程_cmake教程

下载后解压

cmake(1) -- 基础教程_cmake教程_02

1.开始

实验环境:wsl(windows 子系统)

编辑器: vscode,  进入目录后通过​​code .​​在vscode中打开当前目录

cmake(1) -- 基础教程_cmake教程_03

cmake根据CMakeLists.txt与成makefile,所以需要在当前目录下新建CMakeLists.txt.

一个最简单的CmakeLists.txt文件

一个最简单的CMakeLists.txt需要三行代码:

cmake_minimum_required(VERSION 3.10)
# 项目名称 --> Tutorial
project(Tutorial)
# 添加源文件 --> tutorial.cxx
add_executable(Tutorial tutorial.cxx)

cmake命令(也可以理解成函数,比如: project)是不区分大小写的, 可以是project(),也可以是 PROJECT()甚至是ProJect()

代码中 "#" 开头的表示注释!

源码与编译目录分离

cmake中提倡源码与编译目录分离,在当前Step1再创建一个子目录 build(可以在任何位置)

cmake(1) -- 基础教程_cmake教程_04

编译和连接项目

进入build目录,并执行cmake:

cmake(1) -- 基础教程_cmake教程_05

命令格式:

cmake 项目路径 # 这个路径下必须有一个CMakeLists.txt文件

关于cmake中的注释:​​https://cmake.org/cmake/help/latest/manual/cmake-language.7.html#comments​

执行后build目录下分成以下文件:

cmake(1) -- 基础教程_cmake教程_06

编译和链接可以直接使用make或cmake --build .

make
# 或者
cmake --build .

make 或 cmake --build . 其实都是通过执行当前的Makefile文件来生成可执行文件。

cmake(1) -- 基础教程_cmake教程_07

因为cmake可以跨平台,在某些系统上,比如 windows用的不是执行Makefile不是make而是 mingw32-make, 如果直接用make就不合适了,而cmake --build . 可以根据当前环境选择合适的编译工具。

运行程序

编译成功后生成了Turorial可执行程序,通过阅读代码不难发现,这是一个求某个数平方根的程序:

// A simple program that computes the square root of a number
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <string>

int main(int argc, char* argv[])
{
if (argc < 2) {
std::cout << "Usage: " << argv[0] << " number" << std::endl;
return 1;
}

// convert input to double
const double inputValue = atof(argv[1]);

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

cmake(1) -- 基础教程_cmake教程_08

版本号与配置文件

程序版本号一般是在一个配置头文件中以宏的方式定义如:

#define Tutorial_VERSION_MAJOR 1
#define Tutorial_VERSION_MINOR 0

cmake为我们提供了一种更为灵活的方式。

1.project命令指定项目名称和版本号:

project(Tutorial VERSION 1.0)

2.中间文件

cmake中的版本号需要传给代码使用,cmake不能直接将配置信息直接传给.h文件,它是通过一个中间文件(​​xxx.h.in​​)然后根据xxx.h.in生成xxx.h

// TutorialConfig.h.in

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

3.转换和头文件包含路径

# CMakeLists.txt
configure_file(TutorialConfig.h.in TutorialConfig.h)

在build目录下再次执行cmake...发现在build目录中生成了TutorialConfig.h

cmake(1) -- 基础教程_cmake教程_09

其内容:

cmake(1) -- 基础教程_cmake教程_10

为了在代码中使用版本号,在Tutorial.cxx中包含

#include "TutorialConfig.h"

然后:

cmake ..
cmake --build .

报错了:

cmake(1) -- 基础教程_cmake教程_11

需要把配置文件所在目录添加到搜索目录中:

target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
)

if (argc < 2) {
std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "."
<< Tutorial_VERSION_MINOR << std::endl;

std::cout << "Usage: " << argv[0] << " number" << std::endl;
return 1;
}

cmake(1) -- 基础教程_cmake教程_12

指定C++标准

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

要保证这两行代码放在add_executable之前

c++11中std命名空间中添加了很多标准函数如: std::stod,  std::sqrt。

const double inputValue = std::stod(argv[1]);
// calculate square root
const double outputValue = std::sqrt(inputValue);

重新编译并执行...

cmake(1) -- 基础教程_cmake教程_13

小结

  • 我们从一个最简单的只有三行的CMakeLists.txt开始
  • 添加版本号:project(Tutorial VERSION 1.0)
    中间需要用.in文件过渡然后调用:configure_file(TutorialConfig.h.in TutorialConfig.h)
    最后通过:target_include_directories,添加头文件路径,而 PROJECT_BINARY_DIR是cmake的内置变量表示编译目录全路径
  • 指c++ 标准
    set(CMAKE_CXX_STANDARD 11)
    set(CMAKE_CXX_STANDARD_REQUIRED True)​
    通过设置两个变量实现,这两句代码需要放到add_executable之前。

最后附上Tutorial.cxx和CMakelists.txt的代码

# CMakelists.tx
cmake_minimum_required(VERSION 3.10)
project(Tutorial VERSION 1.0)
# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
add_executable(Tutorial tutorial.cxx)

target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
)
configure_file(TutorialConfig.h.in TutorialConfig.h)
// Tutorial.cxx
// A simple program that computes the square root of a number
#include <cmath>
#include <iostream>
#include <string>
#include "TutorialConfig.h"

int main(int argc, char* argv[])
{
if (argc < 2) {
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
const double outputValue = std::sqrt(inputValue);
std::cout << "The square root of " << inputValue << " is " << outputValue
<< std::endl;
return 0;
}

2.添加库

在一个大工程中,通常分将某个功能模块拆分成单独的模块(动态库或静态库)。

在这一小节中,我们将添加一个名为​​MathFunctions的库。​

新建子目录和添加文件

新建目录:​​MathFunctions​​目录中包含:

  • MathFunctions.h
  • mysqrt.cxx

MathFunctions目录可以在Step2中找到,将它们复制到Step1中。

// MathFunctions.h
double mysqrt(double x);
// mysqr.cxx
#include <iostream>

// a hack square root calculation using simple operations
double mysqrt(double x)
{
if (x <= 0) {
return 0;
}

double result = x;

// do ten iterations
for (int i = 0; i < 10; ++i) {
if (result <= 0) {
result = 0.1;
}
double delta = x - (result * result);
result = result + 0.5 * delta / result;
std::cout << "Computing sqrt of " << x << " to be " << result << std::endl;
}
return result;
}

函数mysqrt作用是求一个数的平方根,具体算法就不深究了...

子目录中的CMakeLists.txt

add_library(MathFunctions mysqrt.cxx)

为了使用这个新库在顶层的CMakeLists.txt中还要做一些事:

  • 调用​​add_subdirectory(MathFunctions)​​,子目录将得到编译
  • 调用 ​​target_link_libraries​​将库链接到工程中,要在add_executable之后添加这句话
  • 需要在工程中使用到MathFunctions.h,所以需target_include_directories

cmake_minimum_required(VERSION 3.10)
project(Tutorial VERSION 1.0)

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

add_subdirectory(MathFunctions)

add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial PUBLIC MathFunctions)

target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
"${PROJECT_SOURCE_DIR}/MathFunctions"
)

configure_file(TutorialConfig.h.in TutorialConfig.h)

在tutorial.cxx中使用mysqrt函数

// A simple program that computes the square root of a number
#include <cmath>
#include <iostream>
#include <string>
#include "TutorialConfig.h"
#include "MathFunctions.h"

int main(int argc, char* argv[])
{
if (argc < 2) {
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
const double outputValue = mysqrt(inputValue);
std::cout << "The square root of " << inputValue << " is " << outputValue
<< std::endl;
return 0;
}

编译运行

cmake(1) -- 基础教程_cmake教程_14

可选的MathFunctions库

MatchFunctions库在linux平台上运行良好,假设这个库在windows下不能运行,我们需要一些手段禁用这个库。其实现原理和添加版本号一样:

1.在顶层的CMakeLists.txt中添加

option(USE_MYMATH "Use tutorial provided math implementation" ON)

option定义一个选项,名称为:USE_MYMATH, 第二个参数是描述信息,第二个参数是取值(ON/OFF)

代码中USE_MYMATH的取值默认为ON即使用MathFunctions库。

TutorialConfig.h.in中的代码

#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@
#cmakedefine USE_MYMATH

关于cmakedefine在配置文件中的使用:​​https://cmake.org/cmake/help/latest/command/configure_file.html?highlight=cmakedefine​

2.根据USE_MYMATH的取值,选择是否使用MathFunctions库

cmake_minimum_required(VERSION 3.10)
project(Tutorial VERSION 1.0)

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

option(USE_MYMATH "使用自定义的MathFunctions库" ON)

if(USE_MYMATH)
list(APPEND EXTRA_LIBS MathFunctions)
list(APPEND EXTRA_INCS "${PROJECT_SOURCE_DIR}/MathFunctions")
add_subdirectory(MathFunctions)
endif()

add_executable(Tutorial tutorial.cxx)

target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})

target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
"${EXTRA_INCS}"
)

configure_file(TutorialConfig.h.in TutorialConfig.h)

  • if() ... endif() 可以看作是条件语句块
  • list() 则是列表中添加数据, 如果EXTRA_LIBS 之前没定义它的值是空的,引用没有定义的变量在cmake中不会报错

3.在源代码中判断USE_MYMATH

// 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) {
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 = std::sqrt(inputValue);
#endif
std::cout << "The square root of " << inputValue << " is " << outputValue
<< std::endl;
return 0;
}

编译

cmake .. -DUSE_MYMATH=OFF
# 或者 不指定 使用默认值
cmake ..

需要注意的是,配置的值会保存在 cmake 的cache文件中,如果和一次指定了USE_MYMATH值为OFF, 第二次执行cmake .. 时没有指定, USE_MYMATH的值依然为OFF(使用了cache中的值)

3.为库添加使用要求

在顶层的CMakeLists.txt中还记得这样的代码吗?

target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
"${PROJECT_SOURCE_DIR}/MathFunctions"
)

为了使用MathFunctions库,我们还需要知道MathFunctions的目录结构,这显然不是很合理。

我们可以重构:

# MathFunctions/CMakeLists.txt
target_include_directories(MathFunctions
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
)

重构之后顶层CMakeLists.txt为

cmake_minimum_required(VERSION 3.10)
project(Tutorial VERSION 1.0)

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

option(USE_MYMATH "使用自定义的MathFunctions库" ON)

if(USE_MYMATH)
list(APPEND EXTRA_LIBS MathFunctions)
add_subdirectory(MathFunctions)
message("use my math")
endif()

add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})

target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
)

configure_file(TutorialConfig.h.in TutorialConfig.h)

target_include_directories:指定编译给定目标时要使用的包含目录,它的声明如下:

target_include_directories(<target> [SYSTEM] [AFTER|BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

  • 目标指的应该是参数中的target(教程示例中则为MathFunctions)
  • 如果为INTERFACE(示例代码用的就是它),则items将填充到目标的​​INTERFACE_INCLUDE_DIRECTORIES​​​ 的属性,在它的文档中有这么一段描述: “当使用target_link_libraries()指定目标依赖项时,CMake将从所有目标依赖项中读取此属性来确定使用者的编译属性”
    说得是相当的隐晦...

4.安装

如果在linux上有通过源码方式安装软件的经历对于make install 应该很熟悉!

cmake 为这一功能做了更好封装...

安装规则

# MathFunctions/CMakeLists.txt
install(TARGETS MathFunctions DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)
# CMakeLists.txt
install(TARGETS Tutorial DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
DESTINATION include
)

不难看出,install就是将指定的文件copy到 DESTINATION 路径中,FILES要指定具体的文件。

cmake ..
cmake --build .
cmake --install . --prefix "/home/luan/cmake_tutorial/cmake-3.24.1-tutorial-source/Step1/build/inistall"

instll的文件结构

cmake(1) -- 基础教程_cmake教程_15

关于测试

其实还有一个测试的主题,但是弄了半天,没弄懂...,用到的指令是ctest(cmake的一个测试驱动),感觉这个功能没有存在的必要,在编译工具中写一堆的测试代码,怎么都觉得很怪异...

推荐一个比较好用的c++设置框架(gmock ): ​​http://google.github.io/googletest/​

5.系统自检?


​https://cmake.org/cmake/help/latest/guide/tutorial/Adding%20System%20Introspection.html​

说得通俗一点就是去执行一个操作,根据这个操作能否执行成功从而判断系统是否有这个功能...

教程中就是定义一段代码,然后调用一个函数,根据执行结果确定系统中是否有这样的函数。

6.自定义命令和生成文件

假设我们现要编译的平台目标是一个运算能力很差的单片机中,调用log 或者 exp 需要花费大量的运算时间...

为了提高单片机的性能,我们可以用空间来换点时间,事先算好一些常用的数的平方根然后存到一张表中,把这张表的数据和单片机的程序一起编译链接,在单片机需要计算数的平方根时,先查表如果查不到再去计算...

cmake提供了​​add_custom_command​​指,可以用它完来成上述过程.

# MathFunctions/CMakeLists.txt
add_executable(MakeTable MakeTable.cxx)

我们需MakeTable产生表格,MakeTable.cxx的内容如下示

// A simple program that builds a sqrt table
#include <cmath>
#include <fstream>
#include <iostream>

int main(int argc, char* argv[])
{
// make sure we have enough arguments
if (argc < 2) {
return 1;
}

std::ofstream fout(argv[1], std::ios_base::out);
const bool fileOpen = fout.is_open();
if (fileOpen) {
fout << "double sqrtTable[] = {" << std::endl;
for (int i = 0; i < 10; ++i) {
fout << sqrt(static_cast<double>(i)) << "," << std::endl;
}
// close the table with a zero
fout << "0};" << std::endl;
fout.close();
}
return fileOpen ? 0 : 1; // return 0 if wrote the file
}

添加生成Table.h的命令

# MathFunctions/CMakeLists.txt
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
DEPENDS MakeTable
)

MathFunctions要依赖于Table.h

# MathFunctions/CMakeLists.txt
add_library(MathFunctions
mysqrt.cxx
${CMAKE_CURRENT_BINARY_DIR}/Table.h
)

依赖关系: MathFunctions --> Table.h  --> add_custom_command --> MakeTable --> add_executable(MakeTable MakeTable.cxx)

MathFunctions要在代码中使用Table.h,所以需要把它的目录添加进来

# MathFunctions/CMakeLists.txt
target_include_directories(MathFunctions
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
)

最后就是修改代码,从表中查找...

// MathFunctions/mysqrt.cxx
double mysqrt(double x)
{
if (x <= 0) {
return 0;
}

// use the table to help find an initial value
double result = x;
if (x >= 1 && x < 10) {
std::cout << "Use the table to help find an initial value " << std::endl;
result = sqrtTable[static_cast<int>(x)];
}

// do ten iterations
for (int i = 0; i < 10; ++i) {
if (result <= 0) {
result = 0.1;
}
double delta = x - (result * result);
result = result + 0.5 * delta / result;
std::cout << "Computing sqrt of " << x << " to be " << result << std::endl;
}

return result;
}

最后是编译运行,看结果 ...

7.打包安装

在第4个教程安装我们生成了一个安装目录,这一节的作用就是把这个目录作成压缩包,之后方便发其他人使用。

在文件末尾添加如下代码:

# CMakeLists.txt
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}")
set(CPACK_SOURCE_GENERATOR "TGZ")
include(CPack)
# 在build目录下
cmake ..
cmake --build .
cpack # 打包命令

生成两个文件: Tutorial-1.0-Linux.tar.gz Tutorial-1.0-Linux.tar.Z,这两个文件解压后的内容是一样的:

├── bin
│ └── Tutorial
├── include
│ ├── MathFunctions.h
│ └── TutorialConfig.h
└── lib
└── libMathFunctions.a

8.添加对测试仪表板的支持?

​https://cmake.org/cmake/help/latest/guide/tutorial/Adding%20Support%20for%20a%20Testing%20Dashboard.html​

测试相关的...,跳过!

9.静态库还是动态库?

变量 ​​BUILD_SHARED_LIBS​​ 可以控制 add_library的默认行为

option(BUILD_SHARED_LIBS "Build using shared libraries" OFF)

教程例子,搞得太复杂了,不容易理解,为我此另外写了一个测试例子

# CMakeLists.txt

cmake_minimum_required(VERSION 3.0.0)
project(ss VERSION 0.1.0)
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
add_executable(ss main.cpp)
add_subdirectory(print)
target_link_libraries(ss print)


install(TARGETS ss DESTINATION bin)

set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)

第5行,BUILD_SHARED_LIBS默认设置为ON

// main.cpp
#include <iostream>
#include "print.h"

int main(int, char**) {
std::cout << "Hello, world!\n";
print();
#ifdef MY_PRINT
std::cout << "main my print" << std::endl;
#endif
}

第8到第10,是为了测试target_compile_definitions, MY_PRINT 在print模块中有操作。

子目录print

# print/CMakeLists.txt
add_library(print print.cpp)
target_include_directories(print
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
)
target_compile_definitions(print PUBLIC MY_PRINT)

install(TARGETS print DESTINATION bin)
// print/print.cpp
#include <stdio.h>
void print()
{
printf("hello\r\n");
#ifdef MY_PRINT
printf("my printf \r\n");
#endif
}

编译运行:

cmake(1) -- 基础教程_cmake教程_16

先说动态库:

cmake(1) -- 基础教程_cmake教程_17

so被生成了,这是option(BUILD_SHARED_LIBS "Build using shared libraries" ON)的作用

之后是target_compile_definitions:

myprint 和 main my print 都有打印,说明MY_PRINT宏在print和顶层模块都可见。

和c++的继承一样,PRIVATE PUBLIC INTERFACE 是用来控制宏的可见性,经测试得到以下结论:

  • PRIVATE -- 只在target有效
  • PUBLIC -- 有target和使用者(如本例中的main.cpp)有效
  • INTERFACE -- 只在使用者有效