抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

CMake

快速入门

一个大项目(Project)内嵌多个子项目(SubProject)

一个子项目内有src、include、CMakeLists.txt,其中有一个子项目中有main.cpp

C++项目结构

最外面的CMakeLists.txt,负责连接所有子项目:

cmake_minimum_required(VERSION 3.20)
project(Project)
set(CMAKE_CXX_STANDARD 14)
add_subdirectory(subProject1)
add_subdirectory(subProject2)

subProject1(main.cpp所在的子项目)下面的CMakeLists.txt:

file(GLOB_RECURSE srcs CONFIGURE_DEPENDS src/*.cpp include/*.h)
add_executable(subProject1 ${srcs})
target_include_directories(subProject1 PUBLIC include)
target_link_libraries(subProject1 PUBLIC subProject2)

subProject2下面的CMakeLists.txt:

file(GLOB_RECURSE srcs CONFIGURE_DEPENDS src/*.cpp include/*.h)
add_library(subProject2 STATIC ${srcs})
target_include_directories(subProject2 PUBLIC include)

生成sln项目

$cmake -G "Visual Studio 16 2019"

用vs打开sln项目能看到2+n个项目,其中

  • ALL_BUILD:编译该项目会编译整个工程
  • ZERO_CHECK:监视CMakeLists.txt的变化,一旦改变会告诉编译器重新构建工程

或者可以用make构建项目

$cmake -H. -Bbuild

一:CMake基础语法

CMakeLists.txt

我们将CMake指令放在CMakeLists.txt文件中

#设置CMake所需最低版本
cmake_minimum_required(VERSION 3.20)
#设置项目名称为CMakeStudy,支持的语言为C++(CXX表示C++)
project(CMakeStudy LANGUAGES CXX)
#设置创建的新目标名称:一个名叫CMakeStudy的可执行文件
#这个可执行文件是通过编译和链接源文件main.cpp生成的
add_executable(CMakeStudy main.cpp)
  • CMake语言不区分大小写,但参数区分大小写
  • CMake的缺省默认语言为C++

构建

写好CMakeLists.txt文件后,在命令行中输入:

$cmake -H. -Bbuild

这个命令会搜索当前目录下的根CMakeLists.txt文件,创建一个build目录,在其中生成所有的代码

然后再build目录中输入命令,以完成编译

$cmake --build .

一般我们不会在源码内部构建,因为这会污染源码的目录树

链接

如果项目中有多个文件,如

链接

可以改目标生成

add_executable(hello main.cpp Message.cpp Message.h)

但是这种改法太麻烦了,每添加一个文件就要在后面添一端,最后这东西会特别长

我们可以把这个类编译成一个(静态)库,然后再将库链接到可执行文件中(你还记得c++编译器的编译步骤吗?)

cmake_minimum_required(VERSION 3.20)
project(CMakeStudy LANGUAGES CXX)
#将两个文件编译成库
add_library(message STATIC Message.h Message.cpp)
#目标不变
add_executable(hello main.cpp)
#链接
target_link_libraries(hello message)

此外,我们能在buid目录中找到一个名为/形如libmessage.a的文件,这就是编译得到的静态库

add_library

生成一个名叫message的库

add_library(message STATIC Message.h Message.cpp)
  • 第一个参数是目标名,后续可以使用该名来引用库

  • 第二个参数是库的种类

    • STATIC:静态库
    • SHARED:动态库
    • OBJECT:对象库(将代码编译到可执行文件内部的静态库)
    • MODULE:一种不会链接到项目中任何目标的动态共享对象(DSO),可以运行时动态加载

此外CMake还有一些不会出现在构建系统里的库

  • IMPORTED:项目外部的库,用于对现有依赖项进行构建,认为是不可变的
  • INTERFACE:也是项目之外的库,但是可变
  • ALIAS:对已有的库做别名

条件语句

在讲链接时,我们给出了两种编译方法,我们希望能在两种方式间切换

cmake_minimum_required(VERSION 3.20)
project(CMakeStudy LANGUAGES CXX)
#引入一个新变量USE_LIBRARY,设置为OFF
set(USE_LIBRARY OFF)
#打印信息
message(STATUS "Compile sources into a library? ${USE_LIBRARY}")
set(BUILD_SHARED_LIBS OFF)
#引入一个list变量: _sources,包含两个文件
list(APPEND _sources Message.h Message.cpp)
#判断,若USE_LIBRARY为真,则编译成库
if(USE_LIBRARY)
add_library(message ${_sources})
add_executable(hello main.cpp)
target_link_libraries(hello message)
else()
add_executable(hello main.cpp ${_sources})
endif()

逻辑变量

  • true:1ONYEStrueY、非零数
  • false:0OFFNOfalseNIGNORENOTFOUND、空字符串、以-NOTFOUND为后缀

全局变量

CMake有一些全局变量,修改他们可以起到配置作用,这里设置的

set(BUILD_SHARED_LIBS OFF)

当设置为OFF时,可以使add_library不用传递第二个参数

变量名 含义
CMAKE_RUNTIME_OUTPUT_DIRECTORY .exe、.dll文件的输出路径
CMAKE_ARCHIVE_OUTPUT_DIRECTORY .a文件的输出路径
CMAKE_LIBRARY_OUTPUT_DIRECTORY .so文件的输出路径
CMAKE_CURRENT_SOURCE_DIR 当前CMakeLists.txt所在路径
PROJECT_NAME 项目名字
CMAKE_MODULE_PATH cmake模块所在路径

用户选项

在上面我们引入了一个条件语句,但是是硬编码的。我们希望用户可以控制USE_LIBRARY,于是可以使用option

#set(USE_LIBRARY OFF)
option(USE_LIBRARY "Compile sources into a library" OFF)

将上面下面的set替换为option,运行

$cmake -D USE_LIBRARY=ON

如果是Clion可以配置

Clion-Option

构建类型

类型 有无优化
Debug 没有优化,带调试符号
Release 有优化,没有调试符号
RelWithDebInfo 有少量优化,带调试符号
MinSizeRel 不增加代码大小来优化

编译选项

cmake_minimum_required(VERSION 3.20)
project(CMakeStudy LANGUAGES CXX)

list(APPEND flags "-fPIC" "-Wall")
if(NOT WIN32)
list(APPEND flags "-Wextra" "-Wpedantic")
endif()
#添加一个库
add_library(message
STATIC
Message.h
Message.cpp
)
#为库设置编译选项
target_compile_options(message
PRIVATE
${flags}
)
#添加可执行目标
add_executable(hello main.cpp)
#为可执行目标设置编译选项
target_compile_options(hello
PRIVATE
"-fPIC"
)
#链接
target_link_libraries(hello message)
可见性 含义
PRIVATE 编译选项仅对目标生效,不会传递(hello链接了message,但不会接受message的编译选项)
INTERFACE 编译选项对目标生效,并传递给相关目标
PUBLIC 编译选项对目标和使用它的目标生效

-Wall-Wextra等是警告标志

如果A模块链接了core模块,B模块链接了A模块,如果B模块想用core的头文件,A在链接core时需要是PUBLIC

MSVC

配置MSVC编译选项

if(MSVC)
target_compile_options(my_executable PRIVATE /EHs-c-) # 禁用异常处理
target_compile_options(my_executable PRIVATE /W4 /WX) # 设置警告级别为 4,并将警告视为错误
endif()

循环

foreach(_source ${sources_with_lower_optimization})
get_source_file_property(_flags ${_source} COMPILE_FLAGS)
message(STATUS "Source ${_source} has the following extra COMPILE_FLAGS: ${_flags}")
endforeach()

命令

cmake中可以添加一些自定义命令,常用于实现编译前文件拷贝操作,比如将第三方.dll文件复制到可执行文件到项目根路径

add_custom_command(TARGET ${PROJECT_NAME} 
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E
copy “${xxx.dll}” $<TARGET_FILE_DIR:${PROJECT_NAME}>
)

命令执行时机:

  • POST_BUILD:生成目标文件后
  • PRE_BUILD:编译前
  • PRE_LINK:链接前

执行的命令是Linux Command,比如copy ${source} ${target}echo ${output_string}

搜索

我们不可能将每一个文件都以单文件的形式写进CMakeLists.txt中,于是我们需要按照某种规则搜索所有的文件

file(GLOB_RECURSE SRC_FILES_H "${SOURCE_DIR}/*.h")
file(GLOB_RECURSE SRC_FILES_CPP "${SOURCE_DIR}/*.cpp")
set(ALL_FILES ${SRC_FILES_H} ${SRC_FILES_CPP})

排除SRC中部分文件

list(FILTER SRC EXCLUDE REGEX "/ui/*")
list(FILTER SRC EXCLUDE REGEX "/core/+")

二:环境检查

检查平台

我们要处理如下的C++源码(hello-world.cpp)

std::string say_hello() {
#ifdef IS_WINDOWS
return std::string("Hello from Windows!");
#elif IS_LINUX
return std::string("Hello from Linux!");
#elif IS_MACOS
return std::string("Hello from macOS!");
#else
return std::string("Hello from an unknown system!");
#endif
}

CMake可以加入

#查询操作系统
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
#设置宏
target_compile_definitions(hello-world PUBLIC "IS_LINUX")
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
target_compile_definitions(hello-world PUBLIC "IS_MACOS")
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_compile_definitions(hello-world PUBLIC "IS_WINDOWS")
endif()

检查编译器

if(CMAKE_CXX_COMPILER_ID MATCHES Intel)
...
endif()
if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
...
...

检查处理器架构

if(CMAKE_SIZEOF_VOID_P EQUAL 8)
#64bits
else()
#32bits
endlf()

三:链接外部库

语法

find_package

#查找名为OpenCV的包,如果没找到就报错
find_package(OpenCV REQUIRED)

该函数的本质就是去(先去标准路径)寻找一个包名-config.cmake文件

在mac,找OpenCV找的可能就是

/usr/lib/cmake/OpenCV/OpenCVConfig.cmake

如果你安装的位置不是标准路径,你可以

  • 在build时手动指定-xxx_DIR="aaa/lib/cmake/xxx"
    • 只有第一次指定,只要不删掉build目录,就不需要重新指定
  • 可以在CMakeLists.txt最开头set(xx_DIR "aaa/lib/cmake/xxx")
  • 可以给xxx_DIR设置环境变量

链接静态库

  1. 在项目根目录新建lib文件夹
  2. 将要链接的静态库(test_library.a)复制到lib文件夹中
  3. 找包
find_library(TEXT_LIBRARY test_library lib)
  1. 链接
target_link_libraries(testapp LINK_PUBLIC &{TEST_LIBRARY})

链接动态库

常用库

Eigen

Eigen是一个纯头文件实现的线性代数库,在mac上可以使用brew安装

  1. 安装(记住eigen的版本)
$brew install eigen
  1. 将Eigen链接到系统文件夹(brew一般会自动链接)
$brew link --overwrite eigen
  1. 链接
#寻找Eigen包,并附带包版本
find_package(Eigen3 3.4 REQUIRED CONFIG)
#若找到,则打印信息
if (TARGET Eigen3::Eigen)
message(STATUS "Eigen3 ${EIGEN3_VERSION_STRING} found in ${EIGEN3_INCLUDE_DIR}")
endif ()
#目标
add_executable(path-info main.cpp)
#链接
target_link_libraries(path-info
PUBLIC
Eigen3::Eigen
)
#include <iostream>
#include <Eigen/Dense>

int main(int argc, char **argv){
int dim = std::atoi(argv[1]);
Eigen::MatrixXd A = Eigen::MatrixXd::Random(dim, dim);
std::cout << A;
return 0;
}

brew的用法

这里提一嘴Homebrew,这是一个mac上非常好用的包管理器,可以非常“优雅”地安装软件

brew会把软件安装在/usr/local/Cellar目录

  • 安装目录软链接到/usr/local/opt
  • bin目录执行文件链接到/usr/local/bin中(opt也有可能在根目录)

常用命令

$ brew -v     # 安装完成后可以查看版本
$ brew --help # 简洁命令帮助
$ man brew # 完整命令帮助

$ brew search git # 搜索软件包
$ brew info git # 查看软件包信息
$ brew home git # 访问软件包官方站(用浏览器打开)

$ brew install git # 安装软件包(这里是示例安装Git版本控制)
$ brew uninstall git # 卸载软件包
$ brew list # 显示已经安装的所有软件包
$ brew list --versions # 查看你安装过的包列表(包括版本号)

$ brew update # 同步远程最新更新情况,对本机已经安装并有更新的软件用*标明
$ brew outdated # 查看已安装的哪些软件包需要更新
$ brew upgrade git # 更新单个软件包
$ brew deps php # 显示包依赖

$ brew cleanup # 清理所有已安装软件包的历史老版本
$ brew cleanup git # 清理单个已安装软件包的历史版本
$ brew cleanup -n # 查看哪些软件包要被清除

拷贝动态库

有时为了可拓展性和编译速度,我们会将一个项目切分为多个模块,这些模块编译为dll,然后拷贝到主程序(exe)所在的目录

# 将 OptickCore.dll拷贝到 client.exe所在路径
add_custom_command(TARGET client POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:OptickCore>
$<TARGET_FILE_DIR:client>
)

四:项目

模块

我们可以将一个大的CMake源码分成一个个模块,将这些模块放在cmake文件夹里,后缀为.cmake

如下的项目结构

.
├── cmake
│ └── colors.cmake
└── CMakeLists.txt

cmake/colors.cmake文件内包含了一个色彩输出的定义

macro(define_colors)
if(WIN32)
# has no effect on WIN32
set(ColourReset "")
set(ColourBold "")
set(Red "")
set(Green "")
set(Yellow "")
set(Blue "")
...
else()
string(ASCII 27 Esc)
set(ColourReset "${Esc}[m")
set(ColourBold "${Esc}[1m")
set(Red "${Esc}[31m")
set(Green "${Esc}[32m")
set(Yellow "${Esc}[33m")
set(Blue "${Esc}[34m")
...
endif()
endmacro()

CMakeLists.txt引用

#将/cmake目录添加到路径列表
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
#引入colors.cmake
include(colors)
#使用定义
define_colors()

函数

function(函数名 参数1 参数2)
...
endfunction()

参考资料

Clion CMake

CMake菜谱

小鹏老师

评论