十分钟入门cmake

find_package的用法和原理, get_filename_component已经废弃,现在使用 cmake_path获取路径

CMake内置变量

Variable Info
PROJECT_NAME 由当前project()设置的项目名称
CMAKE_PROJECT_NAME 由project()命令设置的第一个项目的名称,即顶级项目
PROJECT_SOURCE_DIR 当前项目的源目录
PROJECT_BINARY_DIR 当前项目的生成目录
name_SOURCE_DIR 名为“name”的项目的源目录。在本例中,创建的源目录将是sublibrary1_SOURCE_DIR、sublibrary2_SOURCE_DIR和subbinary_SOURCE_DIR
name_BINARY_DIR 名为“name”的项目的二进制目录。在本例中,创建的二进制目录为sublibrary1_BINARY_DIR 、sublibrary2_BINARY_DIR和subbinary_BINARY_DIR。
CMAKE_CURRENT_SOURCE_DIR 当前处理的 CMakeLists.txt 所在的路径
CMAKE_CURRENT_BINARY_DIR target 编译目录;使用 ADD_SURDIRECTORY(src bin) 可以更改此变量的值;SET(EXECUTABLE_OUTPUT_PATH ) 并不会对此变量有影响,只是改变了最终目标文件的存储路径新路径>

设置目标二进制生成路径

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${nodeOutDir})
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${nodeOutDir})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${nodeOutDir})
set(LIBRARY_OUTPUT_PATH ${nodeOutDir})

set(CMAKE_DEBUG_POSTFIX "_d")
set(CMAKE_RELEASE_POSTFIX "_r")

include_directories(${nodeOutDir})
link_directories(${nodeOutDir})

FIND_PACKAGE

引入官方库

为了方便我们在项目中引入外部依赖包,cmake官方为我们预定义了许多寻找依赖包的Module,他们存储在path_to_your_cmake/share/cmake-<version>/Modules目录下。每个以Find<LibaryName>.cmake命名的文件都可以帮我们找到一个包。我们也可以在官方文档中查看到哪些库官方已经为我们定义好了,我们可以直接使用find_package函数进行引用官方文档:Find Modules

我们以curl库为例,假设我们项目需要引入这个库,从网站中请求网页到本地,我们看到官方已经定义好了FindCURL.cmake。所以我们在CMakeLists.txt中可以直接用find_pakcage进行引用。

find_package(CURL)
add_executable(curltest curltest.cc)
if(CURL_FOUND)
target_include_directories(clib PRIVATE ${CURL_INCLUDE_DIR})
target_link_libraries(curltest ${CURL_LIBRARY})
else(CURL_FOUND)
message(FATAL_ERROR ”CURL library not found”)
endif(CURL_FOUND)

对于系统预定义的 Find<LibaryName>.cmake 模块,使用方法一般如上例所示。每一个模块都会定义以下几个变量

<LibaryName>_FOUND
<LibaryName>_INCLUDE_DIR
<LibaryName>_INCLUDES
<LibaryName>_LIBRARY
<LibaryName>_LIBRARIES

引入非官方库

假设此时我们需要引入glog库来进行日志的记录,我们在Module目录下并没有找到 FindGlog.cmake。所以我们需要自行编译安装glog库,再进行引用。

# clone该项目
git clone https://github.com/google/glog.git
# 切换到需要的版本
cd glog
git checkout v0.40

# 根据官网的指南进行安装
cmake -H. -Bbuild -G "Unix Makefiles"
cmake --build build
cmake --build build --target install

此时我们便可以通过与引入curl库一样的方式引入glog库了

Module模式与Config模式

find_package有两种模式,一种是Module模式,也就是我们引入curl库的方式。另一种叫做Config模式,也就是引入glog库的模式。下面我们来详细介绍着两种方式的运行机制。

在Module模式中,cmake需要找到一个叫做Find<LibraryName>.cmake的文件。这个文件负责找到库所在的路径,为我们的项目引入头文件路径和库文件路径。cmake搜索这个文件的路径有两个,一个是上文提到的cmake安装目录下的share/cmake-<version>/Modules目录,另一个使我们指定的CMAKE_MODULE_PATH的所在目录。

如果Module模式搜索失败,没有找到对应的Find<LibraryName>.cmake文件,则转入Config模式进行搜索。它主要通过<LibraryName>Config.cmake or <lower-case-package-name>-config.cmake这两个文件来引入我们需要的库。以我们刚刚安装的glog库为例,在我们安装之后,它在/usr/local/lib/cmake/glog/目录下生成了glog-config.cmake文件,而/usr/local/lib/cmake/<LibraryName>/正是find_package函数的搜索路径之一。另一个路径是/usr/share/<Libraryname>/cmake/<Libraryname>Config.cmake

由以上的例子可以看到,对于原生支持Cmake编译和安装的库通常会安装Config模式的配置文件到对应目录,这个配置文件直接配置了头文件库文件的路径以及各种cmake变量供find_package使用。

编写自己的Find<LibraryName>.cmake模块

非由cmake编译的项目,我们通常会编写一个Find<LibraryName>.cmake,通过脚本来获取头文件、库文件等信息。

在cmake文件夹下新建一个FindAdd.cmake的文件。我们的目标是找到库的头文件所在目录和共享库文件的所在位置。

# 在指定目录下寻找头文件和动态库文件的位置,可以指定多个目标路径
find_path(ADD_INCLUDE_DIR libadd.h /usr/include/ /usr/local/include ${CMAKE_SOURCE_DIR}/ModuleMode)
find_library(ADD_LIBRARY NAMES add PATHS /usr/lib/add /usr/local/lib/add ${CMAKE_SOURCE_DIR}/ModuleMode)

if (ADD_INCLUDE_DIR AND ADD_LIBRARY)
set(ADD_FOUND TRUE)
endif (ADD_INCLUDE_DIR AND ADD_LIBRARY)

这时我们便可以像引用curl一样引入我们自定义的库了。在CMakeLists.txt中添加

# 将项目目录下的cmake文件夹加入到CMAKE_MODULE_PATH中,让find_pakcage能够找到我们自定义的函数库
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake;${CMAKE_MODULE_PATH}")
add_executable(addtest addtest.cc)
find_package(ADD)
if(ADD_FOUND)
target_include_directories(addtest PRIVATE ${ADD_INCLUDE_DIR})
target_link_libraries(addtest ${ADD_LIBRARY})
else(ADD_FOUND)
message(FATAL_ERROR "ADD library not found")
endif(ADD_FOUND)

构建cmake子项目

文件结构如下

├── CMakeLists.txt
├── subbinary
│ ├── CMakeLists.txt
│ └── main.cpp
├── sublibrary1
│ ├── CMakeLists.txt
│ ├── include
│ │ └── sublib1
│ │ └── sublib1.h
│ └── src
│ └── sublib1.cpp
└── sublibrary2
├── CMakeLists.txt
└── include
└── sublib2
└── sublib2.h

[CMakeLists.txt] - 顶级CMakeLists.txt

cmake_minimum_required (VERSION 3.5)

project(subprojects)

# Add sub directories
add_subdirectory(sublibrary1)
add_subdirectory(sublibrary2)
add_subdirectory(subbinary)
  • [subbinary/CMakeLists.txt] - 生成可执行文件
project(subbinary)

# Create the executable
add_executable(${PROJECT_NAME} main.cpp)

# Link the static library from subproject1 using it's alias sub::lib1
# Link the header only library from subproject2 using it's 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
)

如果某个子项目创建库,则其他项目可以通过在target_link_library()命令中调用该项目的名称来引用该库。这意味着你不必引用新库的完整路径,它将作为依赖项被添加。

当从子项目添加库时,从cmake v3开始,不需要使用它们在二进制文件的include目录中添加项目include目录。

这由创建库时target_include_directory()命令中的作用域控制。在本例中,因为子二进制可执行文件链接了subibrary1和subibrary2库,所以它将自动包括${subibrary1_source_DIR}/include${subibrary2_source_DIR}/include文件夹,因为它们是随库的PUBLIC和INTERFACE范围导出的。

[sublibrary1/CMakeLists.txt] - 创建静态库

# Set the project name
project (sublibrary1)

# Add a library with the above sources
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/CMakeLists.txt] - 设置仅含头文件的库

# Set the project name
project (sublibrary2)

add_library(${PROJECT_NAME} INTERFACE)
add_library(sub::lib2 ALIAS ${PROJECT_NAME})

target_include_directories(${PROJECT_NAME}
INTERFACE
${PROJECT_SOURCE_DIR}/include
)

include命令

从文件或模块加载并运行CMake代码.

include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>]
[NO_POLICY_SCOPE])

从给定的文件加载并运行CMake代码。变量读写访问调用者的范围(动态作用域)。如果存在 OPTIONAL ,则如果文件不存在,则不会引发任何错误。如果给定 RESULT_VARIABLE ,则变量 <var> 将设置为已包含的完整文件名,否则将设置为 NOTFOUND 。

如果指定模块而不是文件,则首先在 CMAKE_MODULE_PATH 中搜索名称为 <modulename>.cmake 的文件,然后在CMake模块目录中搜索。

对此有一个例外:如果调用 include() 的文件本身位于CMake内置模块目录中,则首先搜索CMake内置模块目录,然后搜索 CMAKE_MODULE_PATH 。

如果module为本地编译不安装的cmake项目,则从 <modulename>_PATH中寻找cmake项目

获取目录名

get_filename_component

# 当前目录名
get_filename_component(CURRENT_FOLDER ${CURRENT_FOLDER_ABSOLUTE} NAME)

# 上层目录名
get_filename_component(SECOND_FOLDER_ABSOLUTE ${CURRENT_FOLDER_ABSOLUTE} DIRECTORY)
get_filename_component(SECOND_FOLDER ${SECOND_FOLDER_ABSOLUTE} NAME)

string(REGEX REPLACE "(.*)/${CURRENT_FOLDER}$" "\\1" SECOND_FOLDER_ABSOLUTE ${CURRENT_FOLDER_ABSOLUTE})
string(REGEX REPLACE ".*/(.*)" "\\1" SECOND_FOLDER ${SECOND_FOLDER_ABSOLUTE})

cmake_path

在新版本上get_filename_component已经不推荐使用了,cmake_path代替

cmake_path(GET <path-var> ROOT_NAME <out-var>)
cmake_path(GET <path-var> ROOT_DIRECTORY <out-var>)
cmake_path(GET <path-var> ROOT_PATH <out-var>)
cmake_path(GET <path-var> FILENAME <out-var>)
cmake_path(GET <path-var> EXTENSION [LAST_ONLY] <out-var>)
cmake_path(GET <path-var> STEM [LAST_ONLY] <out-var>)
cmake_path(GET <path-var> RELATIVE_PART <out-var>)
cmake_path(GET <path-var> PARENT_PATH <out-var>)

set(path "/a/b")
cmake_path(GET path FILENAME filename)
message("First filename is \"${filename}\"")

# Trailing slash means filename is empty
set(path "/a/b/")
cmake_path(GET path FILENAME filename)
message("Second filename is \"${filename}\"")

#First filename is "b"
#Second filename is ""