目录
一、Cmake 简介
cmake 是一个跨平台、开源的构建系统。它是一个集软件构建、测试、打包于一身的软件。它使用与平台和编译器独立的配置文件来对软件编译过程进行控制。
二、常用命令
1. 指定 cmake 的最小版本
cmake_minimum_required(VERSION 3.4.1)
这行命令是可选的,但如果 CMakeLists.txt 文件中使用了一些高版本 cmake 特有的一些命令的时候,就需要加上这样一行,提醒用户升级到该版本之后再执行 cmake。
2. 设置项目名称
project(demo)
这个命令不是强制性的,但最好加上。它会引入两个变量 demo_BINARY_DIR 和 demo_SOURCE_DIR,同时,cmake 自动定义了两个等价的变量PROJECT_BINARY_DIR 和 PROJECT_SOURCE_DIR。
3. 设置变量
3.1 set 直接设置变量的值
set(SRC_LIST main.cpp test.cpp) #SRC_LIST为变量名,后面是变量值,这里是用来存储源文件
3.2 set 追加设置变量的值
set(SRC_LIST ${SRC_LIST} test.cpp)
3.3 list 追加或者删除变量的值
list(APPEND SRC_LIST test.cpp)
list(REMOVE_ITEM SRC_LIST main.cpp)
4. 添加第三方库或链接其他cmake文件
有时候项目需要使用到很多第三方库,需要根据不同需要添加。
4.1 设置第三方库
在第三方库根目录下编写“LIBNAME-config.cmake"文件,需要按库文件的结构以及使用情况指定:
1)LIBNAME_CONFIG_PATH,获取库目录
2)LIBNAME_FOUND,用于标记是否找到该库文件
3)LIBNAME_INCLUD_DIRS,用于指定头文件路径
4)LIBNAME_LIBS,用于指定库文件路径,若为MSVC环境,需要同时指定debug和optimized两种模式下的路径
get_filename_component(YAML_CPP_CONFIG_PATH "${CMAKE_CURRENT_LIST_FILE}" PATH)
set(YAML_CPP_FOUND True)
set(YAML_CPP_INCLUDE_DIRS ${YAML_CONFIG_PATH}/include/)
if(MSVC)
set(YAML_CPP_LIBS ${YAML_LIBRARIES}
debug
${YAML_CPP_CONFIG_PATH}/lib/Debug/libyaml-cppmdd.lib
optimized
${YAML_CPP_CONFIG_PATH}/lib/Release/libyaml-cppmd.lib)
else()
set(YAML_CPP_LIBS ${YAML_CPP_CONFIG_PATH}/lib/libyaml-cpp)
endif
将第三方库文件夹放置到third_party文件夹下,文件夹名后需要跟版本号,如”yaml-cpp_063“。
在之前项目根目录的CMakeLists.txt中添加第三方库路径:
if(MSVC)
set(YAML-CPP_DIR ${PROJECT_SOURCE_DIR}/third_party/windows/yaml-cpp_063)
elseif(UNIX)
set(YAML-CPP_DIR ${PROJECT_SOURCE_DIR}/third_party/linux/yaml-cpp_063/)
endif()
这样的话,当根目录执行上面这条语句时就会去搜索并执行第三方库的LIBNAME-config.cmake文件,注意整个第三方库的路径需要添加到系统环境变量,否则是搜索不到的。所以为了避免要为每个工程第三方库单独添加系统环境变量,有一种办法是将所有第三方库(包括其他项目使用到的)都放在一个单独的文件夹中,然后将这个文件夹添加到系统环境变量中,但是注意要修改CMakeList.txt中的路径。
# 这里我把第三方库挪到了根目录所在的文件夹下
if(MSVC)
set(YAML-CPP_DIR ${PROJECT_SOURCE_DIR}/../ThirdParty/windows/yaml-cpp_063)
elseif(UNIX)
set(YAML-CPP_DIR ${PROJECT_SOURCE_DIR}/../ThirdParty/linux/yaml-cpp_063/)
endif()
4.2 使用第三方库
第三方库有两种类型,使用方法有些区别。
1)对于与开发平台相关的地方库,比如分为windows/linux两个调用路径的库,首先需要调用find_package,再根据LIBNAME_FOUND判断第三方库是否找到,若找到了,则根据LIBNAME-config.cmake中的变量添加头文件、库文件额USE_LIBNAME定义,用于条件编译;若未找到,则报错。
find_package(YAML-CPP) # 与上一步set中定义的变量名有关,从CMAKE_MODULE_PATH包含的路径中搜索 YAML-CPP文件夹与include()效果一样!
if(YAML_CPP_FOUND) # 判断库目录是否找到
set(INCLUDE_DIRS #{INCLUDE_DIRS}
${YAML_CPP_INCLUDE_DIRS}
)
set(LINK_LIBS #{LINK_LIBS}
${YAML_CPP_LIBS}
)
set(DEFINATIONS #{DEFINATIONS}
USE_YAML
)
else()
message(WARNING "[ModuleName] YAML_CPP not found!")
endif()
工程中使用到第三方库的地方需要进行条件编译。
首先要对头文件进行条件编译:
#ifdef USE_YAML
#include <yaml-cpp/yaml.h>
#endif
其次,要在函数内部进行条件编译,这种情况不具有传递性(对调用这个函数的目标来说,不需要知道有没有使用第三方库),只要处理好函数内部逻辑即可,尽量使用这种方法。
int test()
{
int i = 0;
i = 1;
#ifdef USE_YAML
i = 2;
#endif
return i;
}
2)纯头文件构建的第三方库
也就是说第三方库中只包含头文件,这样只需要调用find_package,需要表示REQUIRED,然后在工程内部正常引用即可,不需要条件编译。
find_package(Eigen Required)
5.添加子模块
5.1父模块中添加子模块
每个添加进去的子模块都会递归调用子模块中的CMakeLists.txt。
注意,添加第三方库与添加子模块两部分可以交换位置,若添加子模块位于添加第三方库后,则子模块可以使用父模块的第三方库信息,否则不能使用。这是由于执行以下函数时,子模块会拷贝一份父模块的变量。
add_subdirectory(preprocessor)
5.2子模块CMakeLists.txt
与根目录的类似,区别有三点:
1)不需要包含project()语句
2)需要收集子模块的库文件,生成lib,注意需要保证生成的lib的名称全局不重复,并将生成的lib与${INCLUDE_DIRS}、${LINK_LIBS}、${DEFINATIONS}关联起来。这一步是将父模块的库路径传递给子模块使用
aux_source_directory(. SRC_LIST)
aux_source_directory(./camera SRC_LIST)
add_library(my_third_party_1 ${SRC_LIST})
target_include_drectories(my_third_party_1
SYSTEM PRIVATE
${INCLUDE_DIRS}
)
target_link_drectories(my_third_party_1
PRIVATE
${LINK_LIBS}
)
target_compile_drectories(my_third_party_1
PRIVATE
${DEFINATIONS}
)
3)将子模块的信息传递给父模块使用
以下语句会把变量往上一级传递。
set(SUB_LIBS ${SUB_LIBS} my_third_party_1 PARENT_SCOPE)
set(SUB_DEFS ${SUB_DEFS} PARENT_SCOPE)
6.设定编译参数
前一个参数时生成的可执行文件名,后有一个是用到的源文件list。
add_executable(demo ${SRC_LIST})
7. 打印信息
message(${PROJECT_SOURCE_DIR})
message("build with debug mode")
message(WARNING "this is warnning message")
message(FATAL_ERROR "this build has many error") # FATAL_ERROR 会导致编译失败
三、常用变量
1. 预定义变量
PROJECT_SOURCE_DIR:工程的根目录
PROJECT_BINARY_DIR:运行 cmake 命令的目录,通常是 ${PROJECT_SOURCE_DIR}/build
PROJECT_NAME:返回通过 project 命令定义的项目名称
CMAKE_CURRENT_SOURCE_DIR:当前处理的 CMakeLists.txt 所在的路径
CMAKE_CURRENT_BINARY_DIR:target 编译目录
CMAKE_CURRENT_LIST_DIR:CMakeLists.txt 的完整路径
CMAKE_CURRENT_LIST_LINE:当前所在的行
CMAKE_MODULE_PATH:定义自己的 cmake 模块所在的路径,SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake),然后可以用INCLUDE命令来调用自己的模块
EXECUTABLE_OUTPUT_PATH:重新定义目标二进制可执行文件的存放位置
LIBRARY_OUTPUT_PATH:重新定义目标链接库文件的存放位置
2. 主要开关选项
BUILD_SHARED_LIBS:这个开关用来控制默认的库编译方式,如果不进行设置,使用 add_library 又没有指定库类型的情况下,默认编译生成的库都是静态库。如果 set(BUILD_SHARED_LIBS ON) 后,默认生成的为动态库
CMAKE_C_FLAGS:设置 C 编译选项,也可以通过指令 add_definitions() 添加
CMAKE_CXX_FLAGS:设置 C++ 编译选项,也可以通过指令 add_definitions() 添加
add_definitions(-DENABLE_DEBUG -DABC) # 参数之间用空格分隔
3.变量作用域
CMakeLists中的变量有两种类型,Normal Variables和Cache Variables,定义方式如下:
set(MY_VAL "666") # Normal Variables
set(MY_CACHE_VAL "666" CACHE STRING INTERNAL) # Cache Variables
3.1Normal Variables
作用域属于当前CMakeLists.txt,但是如果当前文件中包含了add_subdirectory()、include()/find_package()、macro()、function()语句时,会出现两种不同的效果。
1)包含 add_subdirectory()、function()。(本质是值拷贝)
子模块会函数会拷贝一份父模块的变量。
#父模块
cmake_minimum_required(VERSION 3.10)
message("父目录 CMakeLists.txt 文件")
set(MY_VAL "666")
message("第一次在父目录 MY_VAL=${MY_VAL}")
add_subdirectory(src)
message("第二次在父目录,MY_VAL=${MY_VAL}")
#子模块
cmake_minimum_required(VERSION 3.10)
message("进入子目录 src/CMakeLists.txt 文件")
message("在子目录,MY_VAL=${MY_VAL}")
message("退出子目录")
#运行结果
$ cmake .
父目录 CMakeLists.txt 文件
第一次在父目录 MY_VAL=666
进入子目录 src/CMakeLists.txt 文件
在子目录,MY_VAL=666
退出子目录
第二次在父目录,MY_VAL=666
并且子模块中修改父模块的变量,是不会将结果传递给父模块的。
# 修改子模块代码
cmake_minimum_required(VERSION 3.10)
message("进入子目录 src/CMakeLists.txt 文件")
set(MY_VAL "777") # 刚刚加入的
message("在子目录,MY_VAL=${MY_VAL}")
message("退出子目录")
#运行结果
$ cmake .
父目录 CMakeLists.txt 文件
第一次在父目录 MY_VAL=666
进入子目录 src/CMakeLists.txt 文件
在子目录,MY_VAL=777
退出子目录
第二次在父目录,MY_VAL=666
但是可以使用PARENT_SCOPE将修改后的变量传递出去,本质是传递到上一层。
#修改子模块
cmake_minimum_required(VERSION 3.10)
message("进入子目录 src/CMakeLists.txt 文件")
set(MY_VAL "777" PARENT_SCOPE) # 修改处
message("在子目录,MY_VAL=${MY_VAL}")
message("退出子目录")
$ cmake .
父目录 CMakeLists.txt 文件
第一次在父目录 MY_VAL=666
进入子目录 src/CMakeLists.txt 文件
在子目录,MY_VAL=666
退出子目录
第二次在父目录,MY_VAL=777
2)包含include()、macro()。(可以互相调用)
现在在上面的根目录中加入了一个 cmake_modules 目录。目录中有一个 Findtest.cmake 文件。新的目录结构如下:
$ tree
.
├── CMakeLists.txt
├── cmake_modules
│ └── Findtest.cmake
└── src
└── CMakeLists.txt
2 directories, 3 files
在根目录中的 CMakeLists.txt 文件包含的代码如下:
cmake_minimum_required(VERSION 3.10)
message("父目录 CMakeLists.txt 文件")
set(MY_VAL "666")
message("第一次在父目录 MY_VAL=${MY_VAL}")
# 使用 include() 文件的宏
list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake_modules)
include(Findtest) # 从 CMAKE_MODULE_PATH 包含的路径中搜索 Findtest.cmake 文件
#test(${MY_VAL}) # 调用宏
#find_package(test REQUIRED) # 从 CMAKE_MODULE_PATH 包含的路径中搜索 Findtest.cmake 文件 与 include () 两者的效果是一样的!
message("第二次在父目录,MY_VAL=${MY_VAL}")
message("include test=${test_VAL}")
cmake_modules/Findtest.cmake 文件内容如下:
# 该文件定义了一个宏
message("进入 Findtest.cmake 文件")
set(test_VAL "222") # 验证根目录 CMake 文件能够访问这个变量
set(MY_VAL "000") # 测试 include() 效果
# 宏定义
macro(test MY_VA) # 定义一个宏!
set(macro_val "1") # 宏内部定义变量
message("macro is MY_VAL=${MY_VA}")
set(MY_VAL "999") # 直接修改的就是调用该宏所处的文件中的 Normal 变量
endmacro(test)
运行结果如下:
$ cmake .
父目录 CMakeLists.txt 文件
第一次在父目录 MY_VAL=666
进入 Findtest.cmake 文件
第二次在父目录,MY_VAL=000
include test=222
如果把根目录CMakeLists.txt 文件中的调用宏注释取消,输出如下:
$ cmake .
父目录 CMakeLists.txt 文件
第一次在父目录 MY_VAL=666
进入 Findtest.cmake 文件
macro is MY_VAL=000
第二次在父目录,MY_VAL=999
include test=222
macro_val=1
通过这个方式,子模块与父模块之间的变量可以互相调用。
3.2Cache Variables
这属于全局变量,我们在同一个CMake工程中都可以使用。
- Cache变量CMAKE_INSTALL_PREFIX默认值是/usr/local ,这时候如果我们在某个CMakeLists.txt中,仍然使用 set(CMAKE_INSTALL_PREFIX "/usr"),那么此时我们 install 的时候,CMake以后面的/usr作为CMAKE_INSTALL_PREFIX的值,这是因为CMake规定,有一个与Cache 变量同名的Normal变量出现时,后面使用这个变量的值都是以Normal为准,如果没有同名的Normal变量,CMake才会自动使用Cache变量。
- 所有的Cache变量都会出现在CMakeCache.txt文件中。这个文件是我们键入cmake.命令后自动出现的文件。打开这个文件发现,CMake本身会有一些默认的全局Cache变量。例如:CMAKE_INSTALL_PREFIX、CMAKE_BUILD_TYPE、CMAKE_CXX_FLAGSS 等等。可以自行查看。当然,我们自己定义的 Cache 变量也会出现在这个文件中。Cache 变量定义格式为 set(<variable> <value> CACHE STRING INTERNAL)。这里的 STRING可以替换为 BOOL FILEPATH PATH ,但是要根据前面 value 类型来确定。参考。
- 修改Cache变量。可以通过set(<variable> <value> CACHE INSTERNAL FORCE),另一种方式是直接在终端中使用cmake -D var=value ..来设定默认存在的CMake Cache变量。
示例
C++_test
├── modules
│ ├── test
│ │ ├── test.cpp
│ │ └── test.h
│ └── CMakeLists.txt
├── CMakeLists.txt
└── main.cpp
main函数在最外层,包含一个modules模块,modules里的文件有存储在子文件夹下
但是不单独编译成库,而是整个modules打包成一个库。并且还要应用第三方库opencv。
最外层CMakeList.txt如下:
cmake_minimum_required(VERSION 3.0)
project(C++_test)
set(SRC_LIST main.cpp)
# Specify the lib path
set(OpenCV_DIR ${PROJECT_SOURCE_DIR}/../ThirdParty/windows/opencv_320/)
# The default include directory
include_directories(./)
# Function modules
set(CT_LIBS)
set(CT_DEFS)
aux_source_directory(. SRC_LIST)
add_executabel(main ${SRC_LIST})
# Target Info
target_include_directories(main
SYSTEM PRIVATE
${INCLUDE_DIRS}
)
target_link_libraries(main
PRIVATE
${LINK_LIBS}
${CT_LIBS}
)
target_compile_definitions(main
PRIVATE
${DEFINATIONS}
${CT_DEFS}
)
内层CMakeList.txt如下
cmake_minimum_required(VERSION 3.0)
find_package(OpenCV)
# For OpenCV
if(OpenCV_FOUND)
set(INCLUDE_DIRS ${INCLUDE_DIRS}
${OpenCV_INCLUDE_DIRS}
)
set(LINK_LIBS ${LINK_LIBS}
${OpenCV_LIBS}
)
set(DEFINATIONS ${DEFINATIONS}
USE_OPENCV
)
else()
message(WARNING "[modules] OpenCV not found!")
endif()
aux_source_directory(./test MOD_SRC)
add_library(modules ${MOD_SRC})
# Target Info
target_include_directories(main
SYSTEM PRIVATE
${INCLUDE_DIRS}
)
target_link_libraries(main
PRIVATE
${LINK_LIBS}
)
target_compile_definitions(main
PRIVATE
${DEFINATIONS}
)
set(CT_LIBS ${CT_LIBS} modules PARENT_SCOPE)
set(CT_DEFS ${CT_DEFS} ${DEFINATIONS} PARENT_SCOPE)
main.cpp
#pragma once
#include <iostream>
#ifdef USE_OPENCV
#include "modules/test/test.h"
#endif // USE_OPENCV
using namespace modules::test;
int main()
{
#ifdef USE_OPENCV
run();
#endif // USE_OPENCV
system("pause");
return 0;
}
test.h
#ifdef USE_OPENCV
#pragm once
namespace modules{
namespace test{
void run();
} // test
} // modules
#endif // USE_OPENCV
test.cpp
#ifdef USE_OPENCV
#pragma once
#include <iostream>
#include "test.h"
#include <opencv2/opencv.hpp>
namespace modules{
namespace test{
void run()
{
std::cout << "HELLO WORLD." <<std::endl;
}
} // test
} // modules
#endif // USE_OPENCV