0.阅读指南
1.初学者可以从头开始看起,详细学习。
2.本文也可用于有一定基础的读者,根据目录按需获取内容即可。
1.CMake 介绍
CMake 是一个开源、跨平台的构建系统,主要用于软件的构建、测试和打包。
CMake 使用平台无关的配置文件 CMakeLists.txt 来控制软件的编译过程,并生成适用
于不同编译器环境的项目文件。
2.CMake 使用
2.1注释
CMake 使用#进行行注释, 它可以放在任何位置。
CMake 支持⼤写、小写、混合⼤小写的命令。如果在编写 CMakeLists.txt 文件时使
用的工具有对应的命令提示,那么⼤小写随缘即可,不要太过在意。
2.2内部构建和外部构建
当我们执行 cmake . 指令之后源文件所在的目录会多一些文件。这样会导致整个项目目录看起来很混乱,不太容易管理和维护。这其实被称为 内部构建。
但 CMake 强烈推荐的做法是 外部构建。
此时我们可以把生成的这些与项目源码无关的文件统一放到一个对应的目录里边,比
如将这个目录命名为 build,这就叫做 外部构建。
mkdir build
cd build/
cmake .. #.. 指定在上级目录的CMakeLists文件
2.3定义变量
假如我们的项目中存在多个源文件,并且这些源文件需要被反复使用, 每次都直接将
它们的名字写出来确实是很麻烦,此时我们就可以定义一个变量,将文件名对应的字
符串存储起来,在 cmake 里定义变量需要使用 set 指令。
# SET 指令的语法是:
# [] 中的参数为可选项, 如不需要可以不写
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
set(SRC_LIST test1.c test2.c test3.c)
#可以为空
set(SRC "")
2.4指定cpp标准
C++标准对应
有一宏叫做 CMAKE_CXX_STANDARD
方式一:
#在 CMakeLists.txt 中通过 set 命令指定
#增加-std=c++11
set(CMAKE_CXX_STANDARD 11)
#增加-std=c++14
set(CMAKE_CXX_STANDARD 14)
方式二:
#在执行 cmake 命令的时候指定出这个宏的值
#增加-std=c++11
cmake CMakeLists.txt 文件路径 -DCMAKE_CXX_STANDARD=11
#增加-std=c++14
cmake CMakeLists.txt 文件路径 -DCMAKE_CXX_STANDARD=14
2.5指定可执行文件输出的路径
在 CMake 中指定可执行程序输出的路径,也对应一个宏,叫做
EXECUTABLE_OUTPUT_PATH,它的值也是可以通过 set 命令进行设置。
# 定义 HOME 变量,存储一个绝对路径
set(HOME /home/xxx)
# ${} 表示取出变量的值
# 将拼接好的路径值设置给 EXECUTABLE_OUTPUT_PATH 宏
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin)
如果此处指定可执行程序生成路径的时候使用的是相对路径 ./xxx/xxx,那么这个路径
中的 ./ 对应的就是 makefile 文件所在的那个目录。
2.6搜索文件
如果一个项目里边的源文件很多,在编写 CMakeLists.txt 文件的时候不可能将项目目
录的各个文件一一罗列出来,这样太麻烦了。所以在 CMake 中为我们提供了搜索文件
的命令:
1.aux_source_directory
aux_source_directory(< dir > < variable >)
# dir 表示要搜索的目录
# variable 表示将从 dir 目录下搜索到的源文件列表存储到该变量中
2.file
file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)
#GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其
#存储到变量中
#递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存
#储到变量中
#example
file(GLOB SRC_LISTS ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
file(GLOB HEAD_LISTS ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h)
#CMAKE_CURRENT_SOURCE_DIR 宏表示当前访问的 CMakeLists.txt 文件所在的
#路径
2.7包含头文件
在编译项目源文件的时候,很多时候都需要将源文件对应的头文件路径指定出来,这
样才能保证在编译过程中编译器能够找到这些头文件,并顺利通过编译。在 CMake 中
设置头文件路径也很简单,通过命令 include_directories 就可以搞定了。
include_directories(headpath)
2.8生成动态库/静态库
1.生成动态库
add_library(库名称 SHARED 源文件 1 [源文件 2] ...)
在 Linux 系统中,动态库名字分为三部分: lib+库名字+.so,所以库名称也只需要指
定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。
2.生成静态库
add_library(库名称 STATIC 源文件 1 [源文件 2] ...)
在 Linux 系统中,静态库名字分为三部分: lib+库名字+.a,所以库名称只需要指定出
库的名字就可以了,另外两部分在生成该文件的时候会自动填充。
2.9链接动态库/静态库
1.在 cmake 中链接动态库
target_link_libraries(
<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
• target:指定要加载动态库文件的名字
○ 该文件可能是一个源文件
○ 该文件可能是一个动态库文件
○ 该文件可能是一个可执行文件• PRIVATE|PUBLIC|INTERFACE:动态库的访问权限,默认为 PUBLIC
○ 如果各个动态库之间没有依赖关系,无需做任何设置,三者没有没有区别,
一般无需指定,使用默认的 PUBLIC 即可
○ 动态库的链接具有传递性,如果动态库 A 链接了动态库 B、 C;动态库 D 链
接了动态库 A;此时动态库 D 相当于也链接了动态库 B、 C,并可以使用动态库 B、
C 中定义的方法。• PRIVATE|PUBLIC|INTERFACE 的区别:
○ PUBLIC:在 public 后面的库会被 Link 到前面的 target 中,并且里面的符号
也会被导出,提供给第三方使用
○ PRIVATE:在 private 后面的库仅被 link 到前面的 target 中,并且终结掉,第
三方不能感知你调了啥库
○ INTERFACE:在 interface 后面引入的库不会被链接到前面的 target 中,只
会导出符号动态库的链接和静态库是完全不同的:
• 静态库会在生成可执行程序的链接阶段被打包到可执行程序中,所以可执行程序
启动,静态库就被加载到内存中了。
• 动态库在生成可执行程序的链接阶段不会被打包到可执行程序中,当可执行程序
被启动并且调用了动态库中的函数的时候,动态库才会被加载到内存。
因此,在 cmake 中指定要链接的动态库的时候,应该将命令写到生成了可执行文件之
后:
cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
# 添加并指定最终生成的可执行程序名
add_executable(app ${SRC_LIST})
# 指定可执行程序要链接的动态库名字
target_link_libraries(app jsoncpp)
#在 target_link_libraries(app jsoncpp)中: app 表示最终生成的可执行程序的
#名字; jsoncpp表示可执行程序要加载的动态库, 全名为 libjsoncpp.so, 在指定的
#时候一般会掐头(lib)去尾(.so)。
有些时候,当我们去链接第三方的动态库的时候, 如果不指定链接路径,会报错找不
到动态库。此时,我们在生成可执行程序之前,通过命令指定出要链接的动态库的位
置:
link_directories(your_path)
2.在 cmake 中,链接静态库
link_libraries(<static lib> [<static lib>...])
参数为指定要链接的静态库的名字,可以是全名, 也可以是去掉 lib 和.a 之后的名
字。
2.10install 指令
install 指令用于定义安装规则,安装的内容可以包括目标⼆进制、动态库、静态库以
及文件、 目录、脚 本等。
1.安装可执行文件和库
INSTALL(TARGETS targets... [[ARCHIVE|LIBRARY|RUNTIME]
[DESTINATION ] [PERMISSIONS permissions...] [CONFIGURATIONS
[Debug|Release|...]] [COMPONENT ] [OPTIONAL] ] [...])
• 参数中的 TARGETS 后面跟的就是我们通过 ADD_EXECUTABLE 或者
ADD_LIBRARY 定义的目标文件, 可能是可执行⼆进制、动态库、 静态库。
• 目标类型也就相对应的有三种, ARCHIVE 特指静态库, LIBRARY 特指动态库,
RUNTIME 特指可执行目 标⼆进制。
• DESTINATION 定义了安装的路径,如果路径以/开头,那么指的是绝对路径,这
时候 CMAKE_INSTALL_PREFIX 其实就无效了。如果你希望使用
CMAKE_INSTALL_PREFIX 来 定义安装路径,就要写成相对路径,即不要以/开头,那
么安装后的路径就是 ${CMAKE_INSTALL_PREFIX}/...
#example
INSTALL(TARGETS myrun mylib mystaticlib RUNTIME DESTINATION bin
LIBRARY DESTINATION lib ARCHIVE DESTINATION libstatic )
上面的例子会将:
• 可执行⼆进制 myrun 安装到${CMAKE_INSTALL_PREFIX}/bin 目录
• 动态库 libmylib 安装到${CMAKE_INSTALL_PREFIX}/lib 目录
• 静态库 libmystaticlib 安装到${CMAKE_INSTALL_PREFIX}/libstatic 目录
1. 可以使用-D 选项指定 CMAKE_INSTALL_PREFIX 参数, 如 cmake .. -
DCMAKE_INSTALL_PREFIX =/usr
2. 如果没有定义 CMAKE_INSTALL_PREFIX 会安装到什么地方? 可以尝试一
下, cmake ..;make;make install,你会发现 CMAKE_INSTALL_PREFIX 的默认定
义 是/usr/local所以当我们使用源码安装第三方库时如果不希望安装到/usr/local 路径下,可以手动指定安装路径。
2.安装普通文件
INSTALL(FILES files... DESTINATION [PERMISSIONS permissions...]
[CONFIGURATIONS [Debug|Release|...]] [COMPONENT ] [RENAME ]
[OPTIONAL])
可用于安装一般文件,并可以指定访问权限, 文件名是此指令所在路径下的相对路径。
如果默认不定义权限 PERMISSIONS,安装后的权限为: OWNER_WRITE,
OWNER_READ, GROUP_READ,和 WORLD_READ,即 644 权限。
权限:OWNER_READ,OWNER_WRITE,OWNER_EXECUTE,GROUP_READ,GROUP_WRITE,GROUP_EXECUTE,WORLD_READ,WORLD_WRITE,WORLD_EXECUTE,SETUID和SETGID;
3.安装非目标文件的可执行程序
INSTALL(PROGRAMS files... DESTINATION [PERMISSIONS
permissions...] [CONFIGURATIONS [Debug|Release|...]] [COMPONENT ]
[RENAME ] [OPTIONAL])
跟上面的 FILES 指令使用方法一样,唯一的不同是安装后权限为: OWNER_EXECUTE,
GROUP_EXECUTE, 和 WORLD_EXECUTE,即 755 权限。
2.11message 指令
在 CMake 中可以使用命令打印消息,该命令的名字为 message。
message([STATUS|WARNING|AUTHOR_WARNING|FATAL_ERROR|SEND_ERROR]
"message to display" ...)
第一个参数通常不设置, 表示重要消息。
• STATUS : 非重要消息
• WARNING: CMake 警告, 会继续执行
• AUTHOR_WARNING: CMake 警告 (dev), 会继续执行
• SEND_ERROR: CMake 错误, 继续执行,但是会跳过生成的步骤
• FATAL_ERROR: CMake 错误, 终止所有处理过程
#example
# 输出一般日志信息
message(STATUS "。。。")
# 输出警告信息
message(WARNING "。。。")
# 输出错误信息
message(FATAL_ERROR "。。。")
2.12宏定义
在某些程序运行的时候,我们可能会在代码中添加一些宏定义,通过这些宏来控制这
些代码是否生效。在 CMake 中我们也可以使用 add_definitions 来定义宏。
宏 | 功能 |
PROJECT_SOURCE_DI R | 使用 cmake 命令后紧跟的目录,一般是工程的根目录 |
PROJECT_BINARY_DIR | 执行 cmake 命令的目录 |
CMAKE_CURRENT_SOURCE_DIR | 当前处理的 CMakeLists.txt 所在的路径 |
CMAKE_CURRENT_BINARY_DIR | target 编译目录 |
EXECUTABLE_OUTPUT_PATH | 重新定义目标⼆进制可执行文件的存放位置 |
LIBRARY_OUTPUT_PATH | 重新定义目标链接库文件的存放位置 |
PROJECT_NAME | 返回通过 PROJECT 指令定义的项目名称 |
CMAKE_BINARY_DIR | 项目实际构建路径,假设在 build 目录进行的构建,那 么得到的就是这个目录的路径 |
2.13嵌套CMake
如果项目很大,或者项目中有很多的源码目录,在通过 CMake 管理项目的时候如果只
使用一个 CMakeLists.txt,那么这个文件相对会比较复杂,有一种化繁为简的方式就
是给每个源码目录都添加一个 CMakeLists.txt 文件(头文件目录不需要),这样每个文
件都不会太复杂,而且更灵活,更容易维护。
嵌套的 CMake 是一个树状结构,最顶层的 CMakeLists.txt 是根节点,其次都是子节
点。因此,我们需要了解一些关于 CMakeLists.txt 文件变量作用域的一些信息:
• 根节点 CMakeLists.txt 中的变量全局有效
• 父节点 CMakeLists.txt 中的变量可以在子节点中使用
• 子节点 CMakeLists.txt 中的变量只能在当前节点中使用
我们还需要知道在 CMake 中父子节点之间的关系是如何建立的,这里需要用到一个
CMake 命令:
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
• source_dir:指定了 CMakeLists.txt 源文件和代码文件的位置,其实就是指定子目录
• binary_dir:指定了输出文件的路径,一般不需要指定,忽略即可。
• EXCLUDE_FROM_ALL:在子路径下的目标默认不会被包含到父路径的 ALL 目标
里,并且也会被排除在 IDE 工程文件之外。用户必须显式构建在子路径下的目标。
通过这种方式 CMakeLists.txt 文件之间的父子关系就被构建出来了。
下面我们实现一个加减乘除的案例:
根目录中的 CMakeLists.txt 文件内容如下:
cmake_minimum_required(VERSION 3.0)
project(PROJECT)
# 设置头文件目录变量
set(HEAD_PATH ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 添加子目录,并且指定输出文件的路径为 bin 目录
add_subdirectory(src bin)
src 目录中的 CMakeLists.txt 文件内容如下:
cmake_minimum_required(VERSION 3.0)
project(CALC)
# 获取当前目录下源文件列表放到 SRC 变量中
aux_source_directory(./ SRC)
# 包含头文件路径
include_directories(${HEAD_PATH})
# 新增可执行文件
add_executable(calc ${SRC})
然后我们只需要执行顶层cmake即可
2.14其他命令
(1)列表定义添加数据
set(variable_name "")
#可添加一批变量
list(APPEND variable_name content)
(2)字符串内容替换
string(REPLACE ".old" ".new" dest src)
(3)if语句
if () # OR AND (NOT EXISTS) 。。。
endif()
(4)循环
foreach(val vals)
endforeach()
(5)执行外部命令
add_custom_command(
PRE_BUILD #表示在所有其他步骤之前执行自定义命令
COMMAND #要执行的指令名称
ARGS #要执行的指令运行参数选项
DEPENDS #指定命令的依赖项
OUTPUT #指定要生成的目标名称
COMMENT #执行命令时要打印的内容
)
3.实战案例
背景:现在我们有一个项目模块用到了ODB框架(ODB 通过对象 - 关系映射,将对象的操作自动转换为对应的数据库操作。开发人员只需要关注对象本身的操作,比如创建一个新的对象,修改对象的属性,然后通过 ODB 提供的简单接口将这些操作持久化到数据库中),
此项目我们定义了student,class类,需要执行odb命令将其转换成对应的数据库表。
项目层级:
顶层cmake:
cmake_minimum_required(VERSION 3.1.3)
project(all-test)
#指定子cmake文件所在目录
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/test2)
#设置install目录为执行cmake命令所在路径
set(CMAKE_INSTALL_PREFIX ${CMAKE_CURRENT_BINARY_DIR})
test2目录下的子cmake文件:
#添加cmake版本说明
cmake_minimum_required(VERSION 3.22.1)
#声明工程名称
project(test1-odb)
#生成odb文件
# 1. 添加所需的odb映射代码文件名称
#CMAKE_CURRENT_SOURCE_DIR cmake 文件所在目录
set(odb_path ${CMAKE_CURRENT_SOURCE_DIR}/entity)
set(odb_files student.hxx)
# 2. 检测框架代码文件是否已经生成
set(odb_hxx "")
set(odb_cxx "")
set(odb_srcs "")
foreach(odb_file ${odb_files})
# 3. 如果没有生成,则预定义生成指令 -- 用于在构建项目之间先生成框架代码
string(REPLACE ".hxx" "-odb.hxx" odb_hxx ${odb_file})
string(REPLACE ".hxx" "-odb.cxx" odb_cxx ${odb_file})
if(NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/${odb_hxx} OR NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/${odb_cxx})
add_custom_command(
PRE_BUILD #表示在所有其他步骤之前执行自定义命令
COMMAND odb#要执行的指令名称
ARGS -d mysql --std c++11 --generate-query --generate-schema --profile boost/date-time ${odb_path}/${odb_file} #要执行的指令运行参数选项
DEPENDS ${odb_path}/${odb_file}#指定命令的依赖项
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${odb_cxx}#指定要生成的目标名称
COMMENT "生成ODB框架代码文件:" ${CMAKE_CURRENT_BINARY_DIR}/${odb_cxx}#执行命令时要打印的内容
)
endif()
# 4. 将所有生成的框架源码文件名称保存起来 student-odb.cxx classes-odb.cxx
list(APPEND odb_srcs ${CMAKE_CURRENT_BINARY_DIR}/${odb_cxx})
endforeach()
# 5. 获取源码目录下的所有源码文件
set(src_files "")
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/source src_files)
# 6. 声明目标及依赖
add_executable(main ${src_files} ${odb_srcs})
# 7. 设置头文件默认搜索路径
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/entity)
include_directories(${CMAKE_CURRENT_BINARY_DIR})
# 8. 设置需要连接的库
target_link_libraries(main -lodb-mysql -lodb -lodb-boost -lgflags)
# 9. 设置安装路径
INSTALL(TARGETS main RUNTIME DESTINATION bin)
首先我们在顶层cmake文件的同级目录下创建build目录,并进入build目录
执行命令 cmake ..
观察此时build目录生成了一些临时文件
执行 make
我们可以看到成功在build/test2目录下生成了 一系列ODB命令生成的文件。
由于我们是嵌套cmake,所以cmake时在build目录下生成 了一个test2目录,
当我们执行make指令时 build目录下的test2目录是我们的ODB命令执行的路径
所以我们的ODB命令生成的相关文件在build/test2目录下。
(build对应顶层cmake文件,build/test2对应子cmake文件)
最后执行make install
我们在顶层cmake文件中的最后设置了:
set(CMAKE_INSTALL_PREFIX ${CMAKE_CURRENT_BINARY_DIR})
对应为执行cmake命令所在路径
我们在子cmake文件中设置了:
INSTALL(TARGETS main RUNTIME DESTINATION bin)
所以最后我们生成了名为main的可执行文件在 build/bin目录下
感谢阅读,欢迎补充,指正,批评。