关于cmake
https://cmake.org/cmake/help/latest/
CMake是一个管理源代码构建的工具,它先生成makefile然后再调用原生的编译系统编译和连接应用程序。
cmake广泛使用于c和c++,它也可以用于其它语言。
关于安装cmake
下载地址:https://cmake.org/download/#latest
在linux(ubbuntu)上:
安装之后查看版本
windows下是傻瓜式的安装,一直下一步即可~
教程
https://cmake.org/cmake/help/latest/guide/tutorial/index.html
教程就像是编程界的”helloworld“一样,通过实际的例子走完整个工作流程,从而了解cmake的基础用法。
关于示例代码:
https://cmake.org/cmake/help/latest/guide/tutorial/index.html
下载后解压
1.开始
实验环境:wsl(windows 子系统)
编辑器: vscode, 进入目录后通过code .
在vscode中打开当前目录
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(可以在任何位置)
编译和连接项目
进入build目录,并执行cmake:
命令格式:
cmake 项目路径 # 这个路径下必须有一个CMakeLists.txt文件
关于cmake中的注释:https://cmake.org/cmake/help/latest/manual/cmake-language.7.html#comments
执行后build目录下分成以下文件:
编译和链接可以直接使用make或cmake --build .
make 或 cmake --build . 其实都是通过执行当前的Makefile文件来生成可执行文件。
因为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;
}
版本号与配置文件
程序版本号一般是在一个配置头文件中以宏的方式定义如:
#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
其内容:
为了在代码中使用版本号,在Tutorial.cxx中包含
#include "TutorialConfig.h"
然后:
报错了:
需要把配置文件所在目录添加到搜索目录中:
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;
}
指定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);
重新编译并执行...
小结
- 我们从一个最简单的只有三行的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;
}
编译运行
可选的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的文件结构
关于测试
其实还有一个测试的主题,但是弄了半天,没弄懂...,用到的指令是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
}
编译运行:
先说动态库:
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 -- 只在使用者有效