文章目录
前言
本文主要是对 https://github.com/ttroy50/cmake-examples 项目进行学习,并记录。本章主要包含了一些常用的 CMake 基本命令,以及存在子项目时的相关内容。
基础指令
指定基本信息
首先需要注意的一点是,开头需要指定使用的 Cmake 版本,以及给 Project 一个名字:
cmake_minimum_required(VERSION 3.5)
project(test C CXX) # 可以同时指定项目所使用的语言
然后可以根据需求设置相关的编译语言版本:
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
这里就引入 cmake 中 变量(variable)的概念。 其基本都是全大写的名字,防止与用户输入的其他命令混淆。只是这里有一部分 cmake 自己预留的变量名了。
例如:
CMAKE_SOURCE_DIR : 文件根目录
CMAKE_CURRENT_SOURCE_DIR:目前源文件的根目录
PROJECT_SOURCE_DIR:当前cmake 项目的根目录
CMAKE_CURRENT_BINARY_DIR:当前根路径的 build 目录
CMAKE_BINARY_DIR:根目录的 build 目录
PROJECT_BINARY_DIR:目前项目的 build 目录
添加源文件
其中 set 就是我们直接指定,需要完全把需要 set 给变量的内容写出来,没办法通过通配符省事。
set(SOURCES src/A.cpp src/B.cpp)
不可以用下面的形式:
set(SOURCEs src/*.cpp)
否则 SOURCES 打印出来的内容就是 “src/*.cpp” 不具有实际意义。
通配符查找可以使用 file 命令来定义:
file(GLOB SOURCES src/*.cpp)
添加头文件
.
├── CMakeLists.txt
├── include
│ └── Hello.h
└── src
├── Hello.cpp
└── main.cpp
假设文件目录结构如上所示,除了源文件,我们还需要添加头文件才可以完成程序编译。这里可以使用 target_include_directories() 命令添加,它相当于添加编译命令 -I /dir/path。
target_include_directories(target PRIVATE ${PROJECT_SOURCE_DIR}/include)
注意一点,这里第一个参数就是 target,意思是把路径加到哪个目标上,所以我们得现有一个编译的目标,可以是库或者可执行文件。所以定义顺序应该是:
add_executable(hello ${SOURCES})
target_include_directories(hello PRIVATE ${PROJECT_SOURCE_DIR}/include)
编译静态库
编译静态库可以使用下面的命令:
add_library(hello_lib STATIC src/Hello.cpp)
注意这里还是需要在后面添加编译库需要的头文件才可以正常编译。
target_include_directories(hello_lib PUBLIC ${PROJECT_SOURCE_DIR}/include)
上面添加的头文件路径将会用在以下两种情况:
- 编译本项目的库
- 编译其他需要链接此库的目标时
PRIVATE : 路径只能在编译当前库中使用
INTERFACE : 路径还会添加到其他任何链接这个库的目标头文件路径中
PUBLIC:上面的总和,即添加到当前 target 的头文件目录中,同时相关路径也会添加到链接此库的 target 中。
这里添加的时 include 的总目录,所以也推荐在代码中包含头文件也带有路径信息
#include “static/Hello.h”
这样即使有同名的头文件,但是在不同子文件夹中也可以分辨。
链接静态库
相应的,我们添加一个可执行对象,然后使用 target_link_libraries() 命令链接相关库就可以了。这个命令同时还会将相关库的任何设为 PUBLIC 或者是 INTERFACE 的头文件路径一块添加到可执行文件编译命令中。如果上面 target_include_directories 使用的是 PRIVATE,这里在编译可执行文件时会报找不到 static/Hello.h
add_executable(hello src/main.cpp)
target_link_libraries(hello PRIVATE hello_lib)
编译动态库
add_library 同样可以用来编译动态链接库,只将上面你的 STATIC 变为 SHARED 即可。
add_library(hello_lib SHARED src/Hello.cpp)
还可以在下面额外添加一个别名目标,可以在上下文中使用别名:
add_library(hello::lib ALIAS hello_lib)
add_executable(hello src/main.cpp)
target_link_libraries(hello PRIVATE hello::lib)
允许安装(make install)
CMake 支持安装操作,运行用户安装自己的二进制文件,库或者是其他文件。基础的安装路径是被 CMAKE_INSTALL_PREFIX 变量控制的。在 Linux 上默认是 /usr/local
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ryGELP0-1670061089648)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/42b2e5da-0a5d-438b-a41c-3c668fe24c32/Untitled.png)]
上面是目录组织结构,然后下面是相对完整的一个 Cmake 文件:
cmake_minimum_required(VERSION 3.5)
project(install CXX)
set(CMAKE_CXX_STANDARD 17)
message("INSTALL PREFIX: " ${CMAKE_INSTALL_PREFIX})
add_library(hello_lib SHARED src/Hello.cpp)
target_include_directories(hello_lib PUBLIC ${PROJECT_SOURCE_DIR}/include)
add_executable(hello_bin src/main.cpp)
target_link_libraries(hello_bin PRIVATE hello_lib)
install(TARGETS hello_bin DESTINATION bin)
install(TARGETS hello_lib DESTINATION lib)
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include DESTINATION include)
install(FILES cmake-examples.conf DESTINATION etc)
install 实际可以看作是一个拷贝命令,将 install 命令里的内容拷贝到我们指定的目录下,使在环境中也可以运行。
TARGETS 主要用来移动生成的库和二进制文件,
DIRECTORY 用来批量移动路径下的所有头文件
FILES 用来单独移动一个目标文件
以上 DESTNITION 后面跟的路径,完整路径实际就是:${CMAKE_INSTALL_PREFIX}/folder_name
然后运行我们的编译命令:
cmake -B build .
cd build
make install
./hello_bin
上面的安装路径会保存在本地的 install_manifest.txt 文件中。
编译类型
CMake 支持不同的编译类型选项。不同的类型对应了不同程度的优化等级。
- Release : -O3 -DNDEBUG
- Debug : -g
- MinSizeRel:-Os -DNDEBUG
- RelWithDebInfo:-O2 -g -DNDEBUG
在编译时增加 -DCMAKE_BUILD_TYPE 命令来控制。 默认的 CMake 是不加任何优化选项的。对于一些项目可能需要添加默认的编译类型,下面的语句可以做到:
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
message("Setting build type to 'RelWithDebInfo' as none was specified.")
set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE)
# Set the possible values of build type for cmake-gui
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release"
"MinSizeRel" "RelWithDebInfo")
endif()
设置 C++ 标志
CMake 中比较推荐的设置 C++ flags 的方式是使用 target_compile_definitions() 命令。
taeget_compile_definitions(hello_bin PRIVATE EX3)
上面将会在编译时添加 -DEX3 这个命令。 上面的 PRIVATE 同样是控制是否将相关 flags 传递给依赖这个 target 的目标。
上述同样可以通过 target_compile_options() 命令实现。 当然也可以通过 set 命令来设置:
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DEX2")
相似的也可以通过编译时添加相关配置:
cmake -DCMAKE_CXX_FLAGS=-DEX3 .
使用第三方库
几乎所有的重要项目都会依赖第三方库 , CMake 支持使用 find_package() 自动查找这些工具路CMake 将会从 CMAKE_MODULE_PATH 中的路径搜索 名字为 Findxxx.cmake 的文件。
以 Boost 库为例:
find_package(Boost 1.46.1 REQUIRED COMPONENTS filesystem system)
其中:
- Boost : 就是我们要找的目标库。用来查找目标的 FindBoost.cmake 文件。
- 1.46.1 : 是最小的 boost 版本。
- REQUIRED :告诉 module 这个库是必需的,如果找不到就要报错
- COMPONENTS : 是需要使用的 Boost 组件。
运行上面的命令,可能会有下面的内容输出:
-- Found Boost: /usr/include (found suitable version "1.65.1", minimum required is "1.46.1") found components: filesystem system
因为一个库可以包含很多个组件,上面我们只需要使用 system 和 filesystem 组件即可。 在使用上面的同时,会自动定义一个变量是: xxx_FOUND(这里的例子是 Boost_FOUND)来 check 系统中是否找到目标依赖库。
如果找到了也会有
xxx_INCLUDE_DIRS 变量表示依赖库头文件。
xxx_LIBRARY 表示依赖库的路径。
if(Boost_FOUND)
message("Boost FOUND")
include_directories(${Boost_INCLUDE_DIRS})
message(" Boost INCLUDE DIRS: " ${Boost_INCLUDE_DIRS})
else()
message("Boost NOT FOUND!!")
endif()
找到依赖库之后有两种形式可以使用:
add_exeutable(hello_bin main.cpp)
# 使用 alias nanme
target_link_libraries(hello_bin PRIVATE Boost::filesystem)
# 直接使用变量
# target_include_directories(hello_bin PRIVATE ${Boost_INCLUDE_DIRS})
# target_link_libraries(hello_bin PRIVATE ${Boost_FILESYSTEM_LIBRARY} ${Boost_SYSTEM_LIBRARY})
使用 Clang 编译
CMake 支持指定编译器来编译项目:
- CMAKE_C_COMPILER : C 语言的编译器
- CMAKE_CXX_COMPILER:C++ 编译器
- CMAKE_LINKER:链接器
cmake -DCMAKE_CXX_COMPILER=clang++ 。
使用 Ninja 编译
使用 cmake —help 可以看到 cmake 支持的所有生成器。使用 -G xxx 即可指定生成器。
cmake -G Ninja .
加载模块
正如前面在使用第三方库部分提到的,在 CMake v3.5+ 之后,支持使用 alias targets 来加载模块:
target_link_libraries(hello_bin PRIVATE Boost::filesystem)
C++ 标准
在比较老的版本可以使用以下内容来指定 CXX_STANARD:
# $ cmake --version
cmake_minimum_required(VERSION 2.8)
# Set the project name
project (hello_cpp11)
# try conditional compilation
include(CheckCXXCompilerFlag)
CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11)
CHECK_CXX_COMPILER_FLAG("-std=c++0x" COMPILER_SUPPORTS_CXX0X)
# check results and add flag
if(COMPILER_SUPPORTS_CXX11)#
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
elseif(COMPILER_SUPPORTS_CXX0X)#
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")
else()
message(STATUS "The compiler ${CMAKE_CXX_COMPILER} has no C++11 support. Please use a different C++ compiler.")
endif()
# Add an executable
add_executable(hello_cpp11 main.cpp)
随着升级在 v3.1 及之后可以直接通过 set 来指定:
set(CMAKE_CXX_STANDARD 11)
或者支持自动检测:
# set the C++ standard to the appropriate standard for using auto
target_compile_features(hello_cpp11 PUBLIC cxx_auto_type)
# Print the list of known compile features for this version of CMake
message("List of compile features: ${CMAKE_CXX_COMPILE_FEATURES}")
sub-projects
很多大型项目都是由不同对的库和二进制文件组成的,这样就会形成很多文件夹和子项目。
$ tree
.
├── CMakeLists.txt
├── subbinary
│ ├── CMakeLists.txt
│ └── main.cpp
├── sublibrary1
│ ├── CMakeLists.txt
│ ├── include
│ │ └── sublib1
│ │ └── sublib1.h
│ └── src
│ └── sublib1.cpp
└── sublibrary2
├── CMakeLists.txt
└── include
└── sublib2
└── sublib2.h
文件目录结构如上。 其中:
subbinary : 是要编译一个可执行文件
subbinary1 : 是要编译一个依赖库
subbinary2:只有头文件
添加子路径:
一个 CMakeLists.txt 可以包含并调用子路径中的 CMakeLists.txt。 使用 add_subdirectory() 命令。
当使用 project() 命令创建项目时,CMake 会自动定义一系列的变量用表示一些项目的细节,例如:
project(sublibrary1)
就可以使用 ${sublibrary1_SOURCE_DIR} 来表示子路径。相关的变量名如下:
- PROJECT_NAME : 设置的项目名称
- CMAKE_PROJECT_NAME:最上层的项目名称
- PROJECT_SOURCE_DIR:当前项目的目录
- PROJECT_BINARY_DIR:当前目录下的 build 路径
- name_SOURCE_DIR:名字叫 name 的项目的目录
- name_BINARY_DIR:名字叫 name 的二进制文件路径
如果有一个依赖库只有头文件,那么 CMake 支持使用 INTERFACE 来创建一个没有任何输出的 target。
add_library(${PROJECT_NAME} INTERFACE)
然和也要使用 INTERFACE 模式来添加相关的头文件路径,以使后面使用相关 target 的项目都可以找到头文件:
target_include_directories(${PROJECT_Name} INTERFACE ${PROJECT_SOURCE_DIR}/include)
如果一个子项目编译出来的是链接库,则不需要输入路径,可以直接就使用 target_link_libraries() 链接。
target_link_libraries(subbinary PUBLIC sublibrary1)
# 也可是使用别名,使调用更清晰
add_library(sublibrary1 )
add_library(sub::sub1 ALIAS sublibrary1 )
然后使用直接就可以:
target_link_libraries(hello PUBLIC sub::sub1)
下面直接贴出完整的 CMakeLists.txt。
# subprojects
cmake_minimum_required(VERSION 3.5)
project(subprojects)
add_subdirectory(sublibrary1)
add_subdirectory(sublibrary2)
add_subdirectory(sublibrary)
然后下面是各个子目录的 CMakeLists.txt :
sublibrary1 文件夹:
project (sublibrary1)
add_library(${PROJECT_NAME} src/sublib1.cpp)
add_library(sub::lib1 ALIAS ${PROJECT_NAME})
target_include_directories( ${PROJECT_NAME}
PUBLIC ${PROJECT_SOURCE_DIR}/include
)
sublibrary2 文件夹:
project (sublibrary2)
add_library(${PROJECT_NAME} INTERFACE)
add_library(sub::lib2 ALIAS ${PROJECT_NAME})
target_include_directories(${PROJECT_NAME}
INTERFACE
${PROJECT_SOURCE_DIR}/include
)
subbinary 文件夹:
project(subbinary)
# Create the executable
add_executable(${PROJECT_NAME} main.cpp)
# Link the static library from subproject1 using its alias sub::lib1
# Link the header only library from subproject2 using its alias sub::lib2
# This will cause the include directories for that target to be added to this project
target_link_libraries(${PROJECT_NAME}
sub::lib1
sub::lib2
)
可以看到 CMake 版本在最上层指定依次就可以了。