CMake简介
CMake是cross platform make的缩写,从名称中就可以看出它是一个跨平台的编译工具,开发者编写一种与平台无关的CMakeList.txt文件来定制整个编译流程,它会根据不同平台生成不同的Makefile(相对于直接写Makefile来说,CMake的命令语法比较简单),然后编译运行,实现了一个工程能够适配不同平台的一种编译手段。CMake支持in-source构建(二进制文件和源代码在同一个目录中)和out-of-source构建(二进制文件在别的目录里),另外CMake 也支持静态与动态库的构建。CMake官方网站是:www.cmake.org。
Ubuntu 18.04 下CMake的安装方法:
sudo apt install cmake
安装完成后可以查看CMake的版本:
cmake --version
![](https://i-blog.csdnimg.cn/blog_migrate/2ee1bcc9b01bf8ab9143b0db327e3153.png)
CMake工程例程
如果工程结构比较简单的话,我们只要使用一个顶层CMakeLists.txt就够了,但是如果工程结构比较复杂的话,由很多目录组成,这时候把所有构建内容写在一个CMakeLists.txt就显得有点冗长了,这里就推荐使用多个CMakeLists.txt进行构建,同样的从顶层CMakeLists.txt开始执行,通过add_subdirectory把子级的CMakeLists.txt包含进来(一般来说,可以一个文件夹包含一个CMakeLists.txt),从而完成整个工程的构建。下面是博主创建的一个简单的CMake工程结构:
![](https://i-blog.csdnimg.cn/blog_migrate/7990152874d2ecbf15f02559d53189e0.png)
这里推荐使用out-of-source构建的方法,所有的源文件都在src这个文件夹里,所有编译产生的临时文件,和最后的可执行程序都放在build文件夹里,这样的好处是方便对源文件进行版本管理,所有编译的临时文件都是不需要版本追踪的。
首先介绍下编译的流程:
mkdir build // 没有创建build文件夹的话
cd build
cmake .. // 使用上级 ../CMakeLists.txt文件作为起始点
make // 开始编译
使用cmake命令之后,会检查CMakeLists.txt中的语法,配置编译环境,如果正确的话会出现下面类似的输出:
CMakeLists.txt(No.0)
接下来我们来看下最顶层CMakeLists.txt(No.0)里的内容:
cmake_minimum_required(VERSION 2.8)
# create project
project(project_box)
# set compiler
set(CMAKE_CXX_COMPILER "x86_64-linux-gnu-g++-8")
# add compiler flags
add_compile_options(-march=native -O3)
# set binary file output path
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin)
# set lib output path
set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/lib)
message(STATUS "binary dir is" ${EXECUTABLE_OUTPUT_PATH})
message(STATUS "library dir is" ${LIBRARY_OUTPUT_PATH})
# add subdirectory
add_subdirectory(src)
CMakeLists.txt 的内容主要由CMake的命令和注释组成,使用的注意事项:
- CMakeLists.txt 文件名严格区分大小写
- CMake命令可以不区分大小写,大小写混写都行,每条语句结尾不需要加分号
- CMakeLists.txt里的参数严格区分大小写,参数名称只能用字母、数字、下划线、破折号
- 参数使用 ${} 来引用 , 参数之间使用空格分割
- 注释以符号 # 开头
cmake_minimum_required(VERSION 2.8):指定运行这个配置文件所需的CMake最低版本,这个命令一般要求放CMakeLists.txt文件的最开始,在其他命令执行前调用,这是因为后续CMake命令的行为表现可能跟版本有关系。不同的CMakeLists.txt要求的最低版本可以写不同,cmake_minimum_required命令暗含了对cmake_policy的调用,而cmake_policy指定了CMake版本的策略机制,主要是为了保障在不同的CMake版本间的兼容性。同样的道理我们也可以指定最高版本,例如下面的命令就指定了CMake的版本范围:
cmake_minimum_required(VERSION 3.10.0...3.12.0)
project(project_box):设置项目的名称,这个命令会自动生成一些变量,假如整个工程是由多个项目组成,那么引用其他项目的某些变量会变得容易些。比如自动生成了PROJECT_NAME 这个变量,变量值${PROJECT_NAME}为project_box。
set(CMAKE_CXX_COMPILER “”) : set()命令可以设置变量的值,CMAKE_CXX_COMPILER是CMake的环境变量,这个命令是选择CMake的编译器,因为例程是C++的项目所以改变的是CMAKE_CXX_COMPILER这个环境变量,如果是C的项目,那命令可以是下面类似的:
set(CMAKE_C_COMPILER "/usr/local/gcc/bin/gcc")
当然也可以不设置这两个环境变量,CMake会默认去系统目录/usr/bin下找编译器。因为可以灵活选择编译器,这里就体现了CMake这个跨平台的优势,我们可以让同一个代码通过使用CMake在不同平台下编译,实现的方法就是设置一个变量和选择编译器这个命令,比如在CMakeLists.txt里我们可以这么写:
# set compiler
if(NOT TARGET)
set(TARGET "NATIVE")
endif()
if (${TARGET} STREQUAL "NATIVE") # local cpu
set(CMAKE_CXX_COMPILER "x86_64-linux-gnu-g++-8")
elseif (${TARGET} STREQUAL "A9") # cortex-a9
set(CMAKE_CXX_COMPILER "arm-linux-gnueabihf-g++")
endif()
STREQUAL 用于比较字符串,检测TARGET这个变量的值。当没有设置TARGET这个变量时,CMake会使用g++8编译器,对应的就是x86平台,如果TARGET定义为A9,那么CMake就会使用arm-linux-gnueabihf-g++这个交叉编译器,对应的就是arm cortex-a9这个平台,具体使用方法如下:
mkdir build_a9 // 创建针对cortex-a9的build文件夹
cd build_a9
cmake .. -DTARGET=A9 // 使用上级 ../CMakeLists.txt文件作为起始点, 定义TARGET变量
make // 开始编译
add_compile_options(-march=native -O3):这个命令会添加些编译器选项,比如这里-march=native,告诉编译器当前cpu的架构,根据指定的架构对代码进行指令的优化,-O3提供最高级的代码优化。当然这里也可以设置些预处理宏定义给源文件使用(通常用于一些编译开关之类的),比如:
#define VALUE_MACRO // 在源文件中
add_compile_options(-DVALUE_MACRO) // 无参
add_compile_options(-DVALUE_MACRO=0x100) // 有参
设置预处理宏定义也可以通过add_definitions,add_compile_definitions命令实现。
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin) :这个命令设置编译出的二进制文件的存放路径,EXECUTABLE_OUTPUT_PATH和CMAKE_BINARY_DIR也是CMake的环境变量,CMAKE_BINARY_DIR这个变量在terminal里执行完cmake命令之后就设置好了,因为是out-of-place构建,指的是程编译发生的build目录,所以编译完成后二进制文件会放在build/bin文件夹下。如果是in-source的构建方法,那么CMAKE_BINARY_DIR指的就是工程顶层目录。
set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/lib):LIBRARY_OUTPUT_PATH是CMake的环境变量,设置编译出的动态库以及静态库的存放路径,这里为/build/lib。
message(STATUS “binary dir is” ${EXECUTABLE_OUTPUT_PATH}):CMake提供了messge()这个消息命令,在执行make的时候会有提示消息展示。STATUS是消息的类型,我们可以通过消息的类型来控制编译是否继续运行,也可以做些简单的打印提示:
- STATUS = 非重要消息
- WARNING = CMake警告,编译会继续执行
- SEND_ERROR = CMake错误,编译继续执行,但是会跳过生成的步骤
- FATAL_ERROR = CMake错误,终止所有处理过程
add_subdirectory(src):子目录的CMakeLists.txt就是通过这个命令被包含进来,比如这个命令就是去执行src文件夹下CMakeLists.txt。
CMakeLists.txt(No.1)
CMakeLists.txt(No.1)里的内容相对简单,主要是通过add_subdirectory命令将子目录math和app中的CMakeLists.txt包含进来。
cmake_minimum_required(VERSION 2.8)
# include header file
include_directories(.)
# add subdirectory
add_subdirectory(app)
add_subdirectory(math)
include_directories(.):这个命令将指定目录添加到编译器的头文件搜索路径之下,指定的目录被解析成当前源码路径的相对路径。include_directories(.) 就表示包含当前路径的头文件,博主主要是想在main程序里引用box.hpp这个头文件,这样的话就可以在main程序里直接写:
#include "box.hpp"
CMakeLists.txt(No.2)
子目录math下的源文件将被编译成动态库,最后被二进制文件所链接。
cmake_minimum_required(VERSION 2.8)
# include header file
include_directories(.)
# add source file, store in [SRC]
aux_source_directory(. SRC)
# create dynamic library
add_library(box_math SHARED ${SRC})
aux_source_directory(. SRC):这个命令会查找指定路径下的所有源文件名,并将输出结果列表储存在指定的变量中,比较适合有多个源文件存在的情况下使用。比如这里就是查找当前目录下的所有源文件,并且储存到SRC这个变量中。
add_library(box_math SHARED ${SRC}):生成名为box_math的动态库,源文件来自SRC这个变量。SHARED为库的类型,类型为STATIC(静态库)/SHARED(动态库)/MODULE(模块库)之一。
CMakeLists.txt(No.3)
最后是app目录下的CMakeLists.txt,将会生成可执行文件,并且进行链接。
cmake_minimum_required(VERSION 2.8)
# add binary file
add_executable(box_test main.cpp)
# add link library path
link_directories(${LIBRARY_OUTPUT_PATH})
# link library
target_link_libraries(box_test box_math boost_program_options)
add_executable(box_test main.cpp):这个命令用于使用指定的源文件生成可执行文件,比如这里可执行文件的名称是box_test,源文件是main.cpp。
link_directories(${LIBRARY_OUTPUT_PATH}):这个命令用于向链接器添加库文件的搜索路径,以便在链接时找到需要的库文件。因为需要链接本地生成的box_math的动态库,所以路径是LIBRARY_OUTPUT_PATH变量的值。
target_link_libraries(box_test box_math boost_program_options):这个命令用于向给定目标链接依赖库,比如我们向box_test链接box_math动态库,另外代码里用到了boost_program_options这个外部库,也需要进行链接。
总结
这篇博客介绍了一个简单CMake工程样例,给大家有个大致的用CMake构建的概念,CMake的命令介绍的不是很详细,博主将在接下来的博客中详细地分类介绍CMake命令。最后附上整个工程样例的下载链接cmake_box_training。