文章目录
Cmake是一个跨平台的软件构建工具,主要功能是告诉编译器如何工作。
它首先允许开发者编写一种平台无关的 CMakeList.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 和工程文件,如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程。从而做到“Write once, run everywhere”。CMake 是一个比 GNU Make,QT 的 qmake,微软的 MS nmake,BSD Make(pmake) 更高级的编译配置工具。
总体原则
通用项目文件结构
CMake工程文件:
工程目录包含一个
CMakeLists.txt
文件,是构建生成器的入口。
下层子目录的处理,使用add subdirectory()
APl, 该命令会处理下层文件夹中的CMakeLists.txt
文件,
CMake脚本:
使用
<script>.cmake
命名的文件,可以使用命令行cmake -P <script>.cmake
来指定要执行的脚本文件
CMake Module:
使用
<script>.cmake
来命名,文件要在CMAKE _MODULE_PATH
可达的路径,Modules 能够使用include()
命令来装载
文件分布:
- CMakeLists.txt文件与代码同路径存放,共同进行配置管理
- CMake脚本通常放在根目录单独的文件夹中供重用
- 内部Module,一般也放在根目录下,外部Module(不同工程),需设置
CMAKE_MODULE_PATH
示例目录结构如下:
|-- CMakeLists.txt
|-- build
|-- cmake
| -- test.cmake
|-- hello.c
顶层CMakeLists.txt内容如下:
cmake_minimum_required(VERSION 3.12.1)
project(helloworld)
include(cmake/test.cmake)
cmake/test.cmake内容如下:
add_library(hello OBJECT ${
CMAKE_CURRENT_LIST_DIR}/hello.c)
本例中CMAKE_CURRENT_LIST_DIR
的路径为BASE/cmake , 并非CMakeLists.txt所在的路径BASE。
采用Modern CMake风格,优先使用基于target的命令设置编译链接选项。
target_compile_definitions
target_compile_features
target_compile_options
target_include_directories
target_link_directories
target_link_libraries
target_link_options
target_sources
Target的设计趋于“对象”,集合了编译选项,头文件依赖,源代码列表,输出目标类型等信息。也分为“构造函数”,“成员变量”,“成员方法”
- 构造函数:
add_executable()
用于创建一个可执行的Target
add_library()
根据类型,可以创建动态库Shared, 静态库Static,对象列表Object - 成员变量
COMPILE_DEEINITION
INTERFACE_COMPILE_DEFINITIONS
.LINK_LIBRARIES
、INTERFACE LINK LIBRARIES
- 成员方法:使用对象的成员方法,来修改,设置Target的变量。 Object-Oriented是新构建系统的编写实践,而不直接用
set
来设置成员变量
Target的编译选项,可以传递给应用自己的Target。
- PUBLIC
表示相关的设置不仅作用于当前指定的target,而且会随着依赖关系进行传递 - PRIVATE
表示相关的设置仅作用于当前指定的target,不会随依赖关系传递 - INTERFACE
表示相关的设置不作用于当前指定的target,但反而会随依赖关系传递
cmake指令不区分大小写,但规范指定指令用小写字母,变量属性用大写字母。
尽量使用内置变量指定目录
- 用
CMAKE_SOURCE_DIR/CMAKE_CURRENT_SOURCE_DIR
指定源文件路径,
用CMAKE_BINARY_DIR/CMAKE_CURRENT_BINARY_DIR
指定目标文件路径,
用CMAKE_CURRENT_LIST_DIR
指定脚本文件路径,不可混用。 CMAKE_SOURCE_DIR
表示代码根路径,即顶层CMakeLists.txt所在的路径。CMAKE_CURRENT_SOURCE_DIR
表示当前代码路径,即当前CMakeLists.txt所在的路径。CMAKE_BINARY_DIR
表示目标文件根路径,即执行cmake命令时所在的路径。CMAKE_CURRENT_BINARY_DIR
表示当前目标文件路径,即当前CMakeLists.txt所在的路径在目标文件路径下的映射。CMAKE_CURRENT_LIST_DIR
表示当前脚本文件路径。CMAKE_CURRENT_SOURCE_DIR
表示当前CMakeLists.txt所在的路径,而CMakeLists.txt和代码路径严格对应,每个目录有且仅有一个,因此CMAKE_CURRENT_SOURCE_DIR
表示代码路径是准确的。而CMAKE_CURRENT_LIST_DIR
是脚本文件的路径,脚本文件可能会互相调用,它的值在不同脚本文件中取值不同,而脚本文件不只CMakeLists.txt一种,并没有与代码路径一一对应,因此用来表示代码路径不准确。
在 linux 平台下使用 CMake 生成 Makefile 并编译的流程如下:
1.编写 CMake 配置/文件 CMakeLists.txt 。
2.执行命令 cmake PATH
或者 ccmake PATH
生成 Makefile(ccmake 和 cmake 的区别在于前者提供了一个交互式的界面)。其中, PATH 是 CMakeLists.txt 所在的目录。
3.使用 make
命令进行编译。
cmake源码安装
sudo apt install libssl-dev build-essential
git clone https://github.com/Kitware/CMake.git
cd CMake
./bootstrap --prefix=[install_prefix]
sudo make
sudo make install
echo "export PATH=$PATH:[install_prefix]/bin" >> ~/.bashrc
cmake指令
编译选项
add_compile_options
和 target_compile_options
都是CMake中用于设置编译选项的命令,但它们有不同的作用域和使用方式。
add_compile_options
:
add_compile_options([BEFORE] [AFTER] option1 option2 ...)
参数的说明:
[BEFORE] [AFTER]
: 表示在已经存在的编译选项前面或后面添加新的编译选项。[option1 option2 ...]
: 要设置的编译选项列表。
add_compile_options
命令用于为整个项目中的所有目标添加编译选项,可以在项目的 CMakeLists.txt 文件中全局调用。这意味着,所有目标都将使用相同的编译选项。
target_compile_options
:
target_compile_options(target-name [BEFORE] [AFTER]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
参数的说明:
target-name
: 指定要为其设置编译选项的目标名称。[BEFORE] [AFTER]
: 表示在已经存在的编译选项前面或后面添加新的编译选项。<INTERFACE|PUBLIC|PRIVATE>
: 指定编译选项的范围。PRIVATE
和PUBLIC
项将填充<target>
的COMPILE OPTIONS
属性。PUBLIC
和INTERFACE
项将填充的INTERFACE COMPILE OPTIONS
属性。PUBLIC
选项使得链接该目标的目标文件继承该目标的编译选项。[items...]
: 要设置的编译选项列表。
target_compile_options
命令用于向指定的目标添加编译选项。这个命令的作用域限制在当前 CMake 文件和其导入的文件之内。这意味着,您可以针对不同的目标设置不同的编译选项。
target_compile_options和target_compile_features
target_compile_features
和 target_compile_options
都是 CMake 中用于为目标指定编译选项的命令,但它们之间有一些区别。
作用不同
target_compile_features
命令用于指定目标所需的 C++ 特性和最低特性级别,例如 cxx_std_11
,在编译时告诉编译器需要启用 C++11 特性。而 target_compile_options
命令则用于向目标传递编译选项,例如 -Wall
,启用所有警告。
使用方式不同
target_compile_features
命令需要指定目标名称和要启用的特性名称,如下所示:
target_compile_features(<target> <PRIVATE|PUBLIC|INTERFACE> <feature> [...])
编译器支持不同
target_compile_features
只能用于启用编译器已经支持的特性,如果指定了一个编译器无法识别的特性,将会导致错误。而target_compile_options
则可以传递所有编译器支持的选项,包括标准选项和厂商扩展选项。
综上所述,target_compile_features
和 target_compile_options
都是用于为目标指定编译选项的命令,但它们之间有一些区别,主要是作用不同、使用方式不同和编译器支持的选项范围不同。在编写 CMakeLists.txt 时,应根据实际情况选择合适的命令。
预处理定义
add_definitions
和 target_compile_definitions
都可以用于向 CMake 项目添加预处理定义,但它们的作用范围不同。
add_definitions
会为所有的目标(包括可执行文件、静态库和动态库)添加预处理定义。这意味着如果您在项目中调用了 add_definitions
命令,则所有通过该项目构建的二进制文件都将使用该定义。
而 target_compile_definitions
则是为特定的目标(如某个可执行文件或库)设置预处理定义。这意味着您可以选择性地为一个或多个目标添加定义,而不必为整个项目添加定义。
add_definitions(-DENABLE_DEBUG -DABC) # 为所有目标添加-DENABLE_DEBUG -DABC 宏定义
add_executable(example example.cpp)
target_compile_definitions(example PRIVATE BAR BAZ) # 添加 BAR 和 BAZ 宏定义到 example
等效与向C/C++编译器添加-D定义,比如:add_definitions(-DENABLE_DEBUG -DABC)
,参数之间用空格分割。
如果你的代码中定义了#ifdef ENABLE_DEBUG #endif
,这个代码块就会生效。
如果要添加其他的编译器开关,可以通过CMAKE_C_FLAGS
变量和CMAKE_CXX_FLAGS
变量设置。
编译二进制目标文件
编译二进制库文件
通过set(LIBRARY_OUTPUT_PATH <路径>)
来指定共享库输出的位置。
add_library(name [SHARED|STATIC|MODULE|OBJECT]
[EXCLUDE_FROM_ALL] <source1 source2 ... sourceN>)
cmake系统会自动为你生成libname.X
类型有三种:
- SHARED,动态库
- STATIC,静态库
- MODULE,在使用dyld的系统有效,如果不支持dyld,则被当作SHARED对待。
- OBJECT库类型定义了编译给定源文件产生的目标文件的非归档集合(.o文件),目标文件集合可以使用
$<TARGET_OBJECTS:name>
语法作为其他目标的源输入。这是一个生成器表达式,可用于向其他目标提供OBJECT内容:
或者,可以将对象链接到其他目标:add_library(archive OBJECT archive.cpp zip.cpp lzma.cpp) add_library(archiveExtras STATIC $<TARGET_OBJECTS:archive> extras.cpp) add_executable(test_exe $<TARGET_OBJECTS:archive> test.cpp)
add_library(archive OBJECT archive.cpp zip.cpp lzma.cpp) add_library(archiveExtras STATIC extras.cpp) target_link_libraries(archiveExtras PUBLIC archive) add_executable(test_exe test.cpp) target_link_libraries(test_exe archive)
EXCLUDE_FROM_ALL参数的意思是这个库不会被默认构建,除非有其他的组件依赖或者手工构建。
name
作为target
是不能重名的,这意味着,我们无法使用add_library
同时构建同名的静态库和动态库:
add_library(hello STATIC ${
LIBHELLO_SRC})
add_library(hello SHARED ${
LIBHELLO_SRC})
我们想要名字相同的静态库和动态库,因为target名称是唯一的,所以,我们肯定不能通过ADD_LIBRARY
指令来实现了。这时候我们需要用到另外一个指令:
set_target_properties
,其基本语法是:
set_target_properties(target1 target2 ...
PROPERTIES prop1 value1
prop2 value2 ...)
这条指令可以用来设置输出的名称,对于动态库,还可以用来指定动态库版本和API版本。
add_library(hello SHARED ${
LIBHELLO_SRC})
add_library(hello_static STATIC ${
LIBHELLO_SRC})
set_target_properties(hello_static PROPERTIES OUTPUT_NAME "hello")
但是这样也不能同时得到libhello.so/libhello.a两个库。可以发现libhello.a
已经构建完成,位于build/lib
目录中,但是libhello.so
去消失了。这个问题的原因是:cmake在构建一个新的target时,会尝试清理掉其他使用这个名字的库,因此,在构建libhello.a时,就会清理掉libhello.so.
为了回避这个问题,需要再次使用set_target_properties
定义CLEAN_DIRECT_OUTPUT
属性。
set_target_properties(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1)
set_target_properties(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)
这时候,我们再次进行构建,会发现build/lib目录中同时生成了libhello.so
和libhello.a
动态库版本号
为了实现动态库版本号,我们仍然需要使用set_target_properties
指令。
具体使用方法如下:
set_target_properties(hello PROPERTIES VERSION 1.2 SOVERSION 1)
VERSION指代动态库版本,SOVERSION指代API版本。
使用外部共享库和头文件
target_include_directories()
, target_compile_definitions()
and target_compile_options()
命令指定二进制目标的构建规范和使用要求
引入头文件搜索路径
include_directories([AFTER|BEFORE] [SYSTEM] <dir1> <dir2> ...)
这条指令可以用来向工程添加多个特定的头文件搜索路径,路径之间用空格分割,如果路径中包含了空格,可以使用双引号将它括起来,默认的行为是追加到当前的头文件搜索路径的后面,你可以通过两种方式来进行控制搜索路径添加的方式:
CMAKE_include_directories_BEFORE
,通过set这个cmake变量为on,可以将添加的头文件搜索路径放在已有路径的前面。- 通过
AFTER
或者BEFORE
参数,也可以控制是追加还是置前。
为目标添加包含路径
target_include_directories
是一个 CMake 函数,用于为指定的目标添加包含路径。
target_include_directories(<target> [SYSTEM] [AFTER|BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
它有两个参数:
- 目标名称:要为其添加包含路径的目标的名称。
- 包含路径列表:要添加的包含路径列表。
例如,以下代码将 my_lib 目标的包含路径设置为 include 目录:
target_include_directories(my_lib PUBLIC include)
在这个例子中,我们使用了 PUBLIC
修饰符,这意味着 my_lib 的使用者也可以访问这些包含路径(例如,如果另一个库链接到 my_lib,那么这些包含路径也可用于链接到该库)。如果使用 PRIVATE
修饰符,则只有 my_lib 本身才能访问这些路径。
你可以指定多个路径,如下所示:
target_include_directories(my_app PRIVATE
${
PROJECT_SOURCE_DIR}/src
${
PROJECT_SOURCE_DIR}/include
/usr/local/include
)
其中,PROJECT_SOURCE_DIR
是 CMake 中预定义的变量,表示项目根目录的路径。
总之,target_include_directories
可以帮助你轻松地设置和管理包含路径,使得编译器知道在哪里找到头文件。
include_directories
和 target_include_directories
两个 CMake 指令都用于将包含目录包含到构建中,但它们之间有一些区别。
区别如下:
include_directories
是一个全局指令,不需要指定目标,而 target_include_directories
需要指定一个目标来为其添加包含路径。
include_directories
设置的包含路径对整个项目(包括所有目标)都是可见的,而 target_include_directories 可以在特定的目标范围内设置包含路径。这意味着,在使用 target_include_directories 时,你可以精确地控制哪个目标应该具有哪些包含路径。
include_directories 在现代 CMake 中已经被弃用,因为它被认为是过度简化和不够明确。相比之下,target_include_directories 更加明确和易于管理。
综上所述,如果您在现代 CMake 项目中工作,则应该尽可能使用 target_include_directories 来明确控制包含路径。
为target添加共享库
我们现在需要完成的任务是将目标文件链接到libhello,这里我们需要引入两个新的指令
link_directories
和target_link_library
link_directories
的全部语法是:
link_directories(<directory1> <directory2> ...)
这个指令非常简单,添加非标准的共享库搜索路径,比如,在工程内部同时存在共享库和可执行二进制,在编译时就需要指定一下这些共享库的路径。
target_link_libraries
的全部语法是:
target_link_libraries(<target> <library1>
<debug | optimized> <library2>
...)
这个指令可以用来为target添加需要链接的共享库,本例中是一个可执行文件,但是同样可以用于为自己编写的共享库添加共享库链接。
特殊的环境变量
CMAKE_INCLUDE_PATH
和CMAKE_LIBRARY_PATH
是环境变量而不是cmake变量。
使用方法是要在bash中用export或者在csh中使用set命令设置或者CMAKE_INCLUDE_PATH=/home/include cmake ..
等方式。
这里简单说明一下,find_path用来在指定路径中搜索文件名,比如:
find_path(myHeader NAMES hello.h PATHS /usr/include
/usr/include/hello)
这里我们没有指定路径,但是,cmake仍然可以帮我们找到hello.h存放的路径,就是因
为我们设置了环境变量CMAKE_INCLUDE_PATH
。
如果你不使用find_path
,CMAKE_INCLUDE_PATH
变量的设置是没有作用的,你不能指
望它会直接为编译器命令添加参数-I<CMAKE_INCLUDE_PATH>
。
以此为例,CMAKE_LIBRARY_PATH
可以用在find_library
中。
同样,因为这些变量直接为FIND_指令所使用,所以所有使用FIND_指令的cmake模块都会受益。
设置变量
set(<variable> [<value>] [CACHE TYPE DOCSTRING [FORCE]])