从零开始学cmake

find_package和pkg-config的用法和原理, 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)

PKG-CONFIG

pkg-config 就是一个命令工具,可以查询已安装库的基本信息,包括版本号、库路径、头文件路径、编译选项、链接选项等, 是一个在源代码编译时查询已安装的库的使用接口的计算机工具软件。

有些项目安装后不提供cmake文件,之生成了.pc文件,这时候我们需要再cmakelists中找到相关信息

pkg-config --modversion glew  # 查询glew版本号
pkg-config --libs --cflags glew # 获取glew库的基本信息

cmake调用pkg-config

pkg-config是如何找到这些信息的呢?其实,我们在安装第三方库时,会带有一个 .pc文件,比如glew库,.pc文件在本机中位于/usr/local/Cellar/glew/2.1.0_1/lib/pkgconfig/glew.pc,该文件包含了库的基本信息

既然第三方库已经包含了基本信息,那么直接将这些配置信息应用到CMakeLists.txt中即可。具体写法如下:

find_package(PkgConfig)
pkg_search_module(GLEW REQUIRED glew)
MESSAGE(STATUS "glew dirs:" ${GLEW_INCLUDE_DIRS})
MESSAGE(STATUS "glew lib:" ${GLEW_LIBRARIES})
include_directories(${GLEW_INCLUDE_DIRS})
link_directories(${GLEW_LIBRARY_DIRS})
... ...
target_link_libraries(main ${GLUT_LIBRARY} ${OPENGL_LIBRARY} ${GLEW_LIBRARIES})

和find_package异同

#pkg-config 方式
GLEW_INCLUDE_DIRS
GLEW_LIBRARY_DIRS
GLEW_LIBRARIES

#find_package( ) 方式
GLEW_INCLUDE_DIR
GLEW_LIBRARY_DIR
GLEW_LIBRARY

构建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项目

编译选项和链接选项

# 1. 通过include的方式导入本文件:include(path/flags_gccxxx_armxxx_xxx.cmake)
# 2. 导入本文件后,cmake工程中不应该再对下述`CMAKE_XXX`变量进行修改
# 通过`add_library` 或 `add_executable` 等cmake内置函数进行构建的对象,将会默认根据下述选项进行编译
# 3. 对性能要求极高的场景,允许领域去掉"-fno-inline",示例: string(REPLACE "-fno-inline" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS})

## The common options using by both c and cxx
set(COMPILE_FLAGS "-O2 -march=armv7-a -mcpu=cortex-a9 -mlittle-endian -mfloat-abi=soft -mno-sched-prolog -mno-unaligned-access -fPIC -fno-exceptions \
-fno-common -fno-inline -fno-omit-frame-pointer -fno-strict-aliasing -fno-merge-constants \
-freg-struct-return -fno-delete-null-pointer-checks -fstack-protector-strong -pipe\
-Wtrampolines -Wall -Wfloat-equal -Wshadow"
)

## The options for the shared library
set(LINK_FLAGS "-rdynamic -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now")

## the actual options for gcc
set(CMAKE_C_FLAGS "-fsigned-char ${COMPILE_FLAGS}")
set(CMAKE_C_FLAGS_DEBUG "-g")
set(CMAKE_C_FLAGS_RELEASE "-DNDEBUG")

## the actual options for gxx
set(CMAKE_CXX_FLAGS "-std=gnu++1z ${COMPILE_FLAGS}")
set(CMAKE_CXX_FLAGS_DEBUG "-g")
set(CMAKE_CXX_FLAGS_RELEASE "-DNDEBUG")

## The linker options
set(CMAKE_SHARED_LINKER_FLAGS "${LINK_FLAGS}")
set(CMAKE_EXE_LINKER_FLAGS "${LINK_FLAGS}")

各级目录中共享变量

set

一般用set命令定义的变量能从父目录传递到子目录,但opencl与facedetect和facefeature在同级目录,所以用set定义的变量无法共享,要用set(variable value CACHE INTERNAL docstring )这种方式定义的变量会把变量加入到CMakeCache.txt然后各级目录共享会访问到这个变量

set(ICD_LIBRARY "${PROJECT_BINARY_DIR}/lib" CACHE INTERNAL "ICD Library location" )
//"ICD Library location"这个字符串相当于对变量的描述说明,不能省略,但可以自己随便定义

每次运行cmake都会更新这个变量,你会在CMakeCache.txt中找到这个变量

set_property/get_property

使用set_property实现共享变量的方法,不会将变量写入CMakeCache.txt,应该是内存中实现的。

当用set_property定义的property时,第一个指定作用域(scope)的参数设为GLOBAL,这个property在cmake运行期间作用域就是全局的。

然后其他目录下的CMakeLists.txt可以用get_property来读取这个property

set_property(GLOBAL PROPERTY INCLUDE_OPENCL_1_2 "${CMAKE_CURRENT_LIST_DIR}/include/1.2" )

获取目录名

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 ""