CMake进阶教程:库文件构建、发布及列表操作技巧
前言
前面的那篇文章介绍了CMake 的基本概念及其在跨平台项目构建中的重要性。内容涵盖 CMake 的基础语法、常用命令(如 project()
、add_executable()
)以及如何编写简单的 CMakeLists.txt
文件。此外,还演示了如何通过命令行生成构建系统并编译项目,下面我们来更深入的学习。
通过CMake构建库文件
对于一个项目来说有时候我们的需求是将一个项目进行编译成一个可执行程序,但是有的场景我们需要将项目进行构建成库文件给第三方进行使用,库文件又分成两种,分别是静态库和动态库,无论是动态库和静态库cmake都能进行构建,下面就来学习一下通过cmake进行构建库文件。
这里我们首先要进行了解一些前置的知识
无论在window下还是在Linux下,库的命名都是以 lib 开头,紧接着才是库的名字,然后才是库的格式。
但是window和linux下的动态库和静态库的格式是不同的,在window下静态库是以 .lib 结尾的、动态库是以 .dll结尾的;在Linux下静态库是以 .a 结尾的、动态库是以 .so 结尾的。
制作动态库
命令:
add_library(库名称 SHARE 一个或多个源文件)
动态库是有可执行权限的,但是静态库没有(字体颜色)
制作静态库
命令:
add_library(库名称 STATIC 一个或多个源文件)
注意:
1、这个库名称是进行掐头去尾后的名称。
2、一个或者多个源文件不要手动写,用上篇博客中的file或者aux_source_directory命令进行搜索即可。
指定库文件的生成路径
由于动态库是有可执行权限的,所以我们之前学习的设置可执行程序的生成路径的命令:set(EXCUTABLE_OUTPUT_PATH 路径)对于动态库也是适用的。
但是显然这个宏不太清晰,所以建议制定库文件的生成路径使用下面这个命令
set(LIBRARY_OUTPUT_PATH 库文件路径)
#声明cmake的最低版本号
cmake_minimum_required(VERSION 3.15) #这行的版本是可以写也可以不写的,如果不写可能会有警告#命名项目
project(test)#set
#set(SRC main.cpp)
#aux_source_directory(${PROJECT_SOURCE_DIR} SRC)
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
set(CMAKE_CXX_STANDARD 11)
set(LIBRARY_OUTPUT_PATH /home/ys/code/9.7code/lib/)#声明头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include)
#库文件的生成
add_library(Math SHARED ${SRC})
将库文件发布给第三方使用
无论是静态库还是动态库要想进行发布给第三方进行使用,还需要将头文件和库文件一起打包进行发送给第三方,因为库文件本质上就是二进制的源文件。
在程序中链接静态库
在CMake中进行链接静态库的命令
link_libraries(第一个静态库名字 第二个静态库名字 ...)
这里的静态库的名字可以是掐头去尾过后的,也可以是全名 libxxx.a
这里还有一个细节问题需要进行注意
如果将要进行使用的静态库如果没有指定路径,链接器会去环境变量里去寻找。
#声明cmake的最低版本号
cmake_minimum_required(VERSION 3.15) #这行的版本是可以写也可以不写的,如果不写可能会有警告#命名项目
project(test)#变量命名
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
set(CMAKE_CXX_STANDARD 11)
set(EXECUTABLE_OUTPUT_PATH /home/ys/code/9.7code/test/exe/)#声明头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include) #链接静态库
link_libraries(Math)#可执行程序的生成
add_executable(main ${SRC})
指定静态库的路径
link_directors(第一个静态库路径 第二个静态库路径 ...)
#声明cmake的最低版本号
cmake_minimum_required(VERSION 3.15) #这行的版本是可以写也可以不写的,如果不写可能会有警告#命名项目
project(test)#变量命名
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
set(CMAKE_CXX_STANDARD 11)
set(EXECUTABLE_OUTPUT_PATH /home/ys/code/9.7code/test/exe/)#声明头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include) #指定静态库路径
link_directories(${PROJECT_SOURCE_DIR}/lib) #+#链接静态库
link_libraries(Math)#可执行程序的生成
add_executable(main ${SRC})
在程序中链接动态库
在程序中进行连接动态库和在程序中进行链接静态库的命令是差不多的,多了一个前缀target,使用自己进行指定的动态库的时候也需要将路径进行指定出来,否则会出现找不到的情况。
target_link_libraries(需要链接动态库的文件 动态库的访问权限+动态库的名字)
- 需要进行链接动态库的文件可能会有 源文件、动态库文件、可执行文件。
- 动态库的权限有PUBLIC | PRIVATE | interface 默认权限是 PUBLIC,并且动态库是有传递性的。
如何进行理解需要进行链接动态库的文件可能会有 源文件、动态库文件、可执行文件呢?
源文件中可能会使用的动态库的,动态库中也会使用其他动态库,可执行程序中也会使用动态库
如何理解动态库在进行链接的时候具有传递性?
前置知识:如果各个动态库之间没有依赖关系,就不需要进行设置,直接使用默认的PUBLIC即可
如果动态库A,链接了动态库B和动态库C,动态库D链接了动态库A,此时相当于动态库D也链接了动态库B和动态库C。
动态库访问权限的认识
通过动态库的传递进行举例:
如果动态库A,链接了动态库B和动态库C,动态库D链接了动态库A
public: 在public权限下,此时相当于动态库D也链接了动态库B和动态库C,可以随便进行调用
interface:在interface权限下,此时相当于动态库D也链接了动态库B和动态库C,可以随便进行调用,但是动态库D并不知道调用函数的源码
private: 在private权限下,此时相当于只有动态库可以使用动态库B和动态库C中的方法,由于private权限限制,动态库D是不可以调用B和C动态库中的方法。
一个小细节
我们在进行使用target_link_libraries进行链接动态库的时候,这个命令一般放到哪个位置呢?
要把他放到cmake文件中的最后,准确的来说就是放到add_excutable 命令生成可执行程序的后面。为什么这样呢?这就和生成可执行程序进行链接动态库的时机有关系,只有可执行程序中调用动态库中的函数时才需要进行链接动态库。
还是上面使用的程序例子
#声明cmake的最低版本号
cmake_minimum_required(VERSION 3.15) #这行的版本是可以写也可以不写的,如果不写可能会有警告#命名项目
project(test)#变量命名
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
set(CMAKE_CXX_STANDARD 11)
set(EXECUTABLE_OUTPUT_PATH /home/ys/code/9.15code/test_so/exe/)#声明头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include) #指定静态库路径
link_directories(${PROJECT_SOURCE_DIR}/lib) #可执行程序的生成
add_executable(main ${SRC})#链接动态库
target_link_libraries(main Math)
在cmake中进行打印日志信息
message(日志等级 日志信息...)
其中日志等级为
- (无) :重要消息
- STATUS :非重要消息
- WARNING:CMake 警告, 会继续执行
- AUTHOR_WARNING:CMake 警告 (dev), 但是cmake不会进行中止,会继续执行
- SEND_ERROR:CMake 错误, 继续执行,但是会跳过生成的步骤(打印信息步骤)
- FATAL_ERROR:CMake 错误, 终止所有处理过程
cmake打印日志信息的场景
1. 调试和开发阶段
调试变量值
cmake
message("CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")
message("CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}")
message("PROJECT_NAME: ${PROJECT_NAME}")
跟踪执行流程
cmake
message(STATUS "正在配置目标: ${target_name}")
# ... 相关配置代码
message(STATUS "目标配置完成")
2. 条件编译信息输出
检查条件判断结果
cmake
if(SOME_CONDITION)
message("SOME_CONDITION 为真")
else()
message("SOME_CONDITION 为假")
endif()
特性检查结果
cmake
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag("-std=c++17" HAS_CXX17)
if(HAS_CXX17)
message(STATUS "编译器支持 C++17")
else()
message(WARNING "编译器不支持 C++17")
endif()
3. 用户提示和配置反馈
显示重要配置选项
cmake
message(STATUS "构建类型: ${CMAKE_BUILD_TYPE}")
message(STATUS "安装前缀: ${CMAKE_INSTALL_PREFIX}")
message(STATUS "编译器: ${CMAKE_CXX_COMPILER_ID}")
功能启用状态
cmake
option(ENABLE_TESTS "启用测试" ON)
if(ENABLE_TESTS)
message(STATUS "测试功能: 启用")
else()
message(STATUS "测试功能: 禁用")
endif()
4. 错误和警告处理
必要条件检查
cmake
if(NOT REQUIRED_DEPENDENCY_FOUND)
message(FATAL_ERROR "缺少必需的依赖: RequiredDependency")
endif()
弃用警告
cmake
if(USE_DEPRECATED_FEATURE)
message(DEPRECATION "此特性已弃用,将在未来版本中移除")
endif()
5. 模块和函数调试
函数参数检查
cmake
function(my_function arg1 arg2)
message(DEBUG "my_function 被调用,参数: ${arg1}, ${arg2}")
# 函数实现
endfunction()
模块加载信息
cmake
message(VERBOSE "正在加载模块: MyModule.cmake")
include(MyModule)
message(VERBOSE "模块加载完成")|
将这些场景总结成大类
字符串的其他操作
在cmake中存储的变量都是字符串类型,也就意味着在cmake中会进行大量的字符串类型的操作。
数据的拼接
set 命令进行数据拼接
set(变量名1 ${变量名1} ${变量名2} ...)
将变量名1和变量名2以及若干个变量进行拼接后,形成一个新的变量1
这里的变量不只是真正的变量,还可能是常量字符串,比如“hello”,所以说字符串的拼接是 常量、变量之间的组合,进行拼接之后的路径实际绝对路径。
使用 set 命令进行创建创建的字符串拼接本质上字符串之间是通过分号进行间隔的,但是通过打印是看不出来的 例如:set(变量名1 a b c d e),本质就是a;b;c;d;e。
list 命令
进行数据拼接
list(APPEND 变量名1 ${变量名1} ${变量名2} ...)
和使用set 进行字符串拼接一样,这里就不在进行过多的解释。
进行字符串的移除
进行字符串的移除这是非常高频使用的,例如:我们的项目中的源码一般都是放到src目录下的,但是当我们要进行打包成动静态库时,需要将程序入口函数main函数进行移除,这时候就需要进行使用字符串的移除。
list(REMOVE_ITEMREMOVE_ITEM 变量名1 ${变量名1} ${变量名2} ...)
在进行删除的时候,不能仅仅通过文件名,还需要指定绝对路径,因为输出的路径就是绝对路径
对于常用list命令的实操
#声明cmake的最低版本号
cmake_minimum_required(VERSION 3.15) #这行的版本是可以写也可以不写的,如果不写可能会有警告#命名项目
project(test)#变量命名
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)message("=======SRC的路径(start)========")
message("${SRC}")
message("=======SRC的路径(end)========")list(REMOVE_ITEM SRC ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp)
message("=======remove_main_SRC的路径(start)========")
message("${SRC}")
message("=======remove_main_SRC的路径(end)========")set(CMAKE_CXX_STANDARD 11)
set(EXECUTABLE_OUTPUT_PATH /home/ys/code/9.15code/test_so/exe/)#声明头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include) #指定静态库路径
link_directories(${PROJECT_SOURCE_DIR}/lib) #可执行程序的生成
#add_executable(main ${SRC})#链接动态库
#target_link_libraries(main Math)set(SRC "${SRC}" "hello")
message("${SRC}")
list(APPEND SRC "cmake")
message("${SRC}")
list的其他命令
list的其他命令并不常用,这里就不进行详细的介绍,当手册进行记录
读取与查询操作
获取列表长度 (LENGTH)
用于获取一个列表包含的元素数量。
语法
list(LENGTH <list> <output variable>)
参数说明
<list>
:要操作的列表变量名。<output variable>
:用于存储列表长度的新变量名。
示例
set(my_list "apple" "banana" "cherry")list(LENGTH my_list list_len)message("The length of my_list is: ${list_len}") # 输出: The length of my_list is: 3
获取指定索引的元素 (GET)
读取列表中一个或多个指定索引位置的元素。
语法
list(GET <list> <element index> [<element index> ...] <output variable>)
参数说明
<list>
:要操作的列表变量名。<element index>
:元素的索引,可以为多个。- 从 0 开始编号,0 代表第一个元素。
- 支持负数索引,-1 代表最后一个元素,-2 代表倒数第二个,以此类推。
- 如果索引越界,CMake 会报错。
<output variable>
:用于存储结果的新变量,结果本身也是一个列表。
示例
set(my_list "apple" "banana" "cherry" "date")list(GET my_list 0 2 output_elements)message("Elements at index 0 and 2 are: ${output_elements}") # 输出: Elements at index 0 and 2 are: apple;cherrylist(GET my_list -1 last_element)message("The last element is: ${last_element}") # 输出: The last element is: date
连接列表元素为字符串 (JOIN)
将列表中的所有元素用指定的连接符拼接成一个单一的字符串。
语法
list (JOIN <list> <glue> <output variable>)
参数说明
<list>
:要操作的列表变量名。<glue>
:用作连接符的字符串。<output variable>
:用于存储拼接后字符串的新变量名。
示例
set(my_list "apple" "banana" "cherry")list(JOIN my_list " -> " joined_string)message("Joined string: ${joined_string}") # 输出: Joined string: apple -> banana -> cherry
查找元素索引 (FIND)
在列表中搜索指定元素,并返回其首次出现的索引。
语法
list(FIND <list> <value> <output variable>)
参数说明
<list>
:要操作的列表变量名。<value>
:要搜索的元素值。<output variable>
:用于存储结果索引的新变量名。如果找到,返回索引;如果未找到,返回 -1。
示例
set(my_list "apple" "banana" "cherry")list(FIND my_list "banana" index_of_banana)message("Index of 'banana' is: ${index_of_banana}") # 输出: Index of 'banana' is: 1list(FIND my_list "grape" index_of_grape)message("Index of 'grape' is: ${index_of_grape}") # 输出: Index of 'grape' is: -1
修改列表:添加与插入
这类操作会向列表中添加新元素,直接修改原始列表。
在末尾追加元素 (APPEND)
将一个或多个元素追加到列表的末尾。
语法
list (APPEND <list> [<element> ...])
示例
set(my_list "apple" "banana")list(APPEND my_list "cherry" "date")message("Appended list: ${my_list}") # 输出: Appended list: apple;banana;cherry;date
在指定位置插入元素 (INSERT)
在列表的指定索引位置插入一个或多个元素。
语法
list(INSERT <list> <element_index> <element> [<element> ...])
示例
set(my_list "apple" "cherry")list(INSERT my_list 1 "banana")message("Inserted list: ${my_list}") # 输出: Inserted list: apple;banana;cherry
在开头添加元素 (PREPEND)
将一个或多个元素添加到列表的开头(索引 0 的位置),是 list(INSERT <list> 0 ...)
的便捷写法。
语法
list (PREPEND <list> [<element> ...])
示例
set(my_list "banana" "cherry")list(PREPEND my_list "apple")message("Prepended list: ${my_list}") # 输出: Prepended list: apple;banana;cherry
修改列表:移除元素
这类操作会从列表中删除元素,直接修改原始列表。
移除末尾元素 (POP_BACK)
移除并选择性地返回列表的最后一个元素。
语法
list (POP_BACK <list> [<out-var>...])
示例
set(my_list "apple" "banana" "cherry")list(POP_BACK my_list last_element)message("List after pop back: ${my_list}") # 输出: List after pop back: apple;bananamessage("Popped element: ${last_element}") # 输出: Popped element: cherry
移除开头元素 (POP_FRONT)
移除并选择性地返回列表的第一个元素。
语法
list (POP_FRONT <list> [<out-var>...])
示例
set(my_list "apple" "banana" "cherry")list(POP_FRONT my_list first_element)message("List after pop front: ${my_list}") # 输出: List after pop front: banana;cherrymessage("Popped element: ${first_element}") # 输出: Popped element: apple
移除指定值的元素 (REMOVE_ITEM)
从列表中移除所有与指定值匹配的元素。
语法
list (REMOVE_ITEM <list> <value> [<value> ...])
示例
set(my_list "apple" "banana" "cherry" "banana")list(REMOVE_ITEM my_list "banana")message("List after removing item: ${my_list}") # 输出: List after removing item: apple;cherry
移除指定索引的元素 (REMOVE_AT)
从列表中移除一个或多个指定索引位置的元素。
语法
list (REMOVE_AT <list> <index> [<index> ...])
示例
set(my_list "apple" "banana" "cherry" "date")list(REMOVE_AT my_list 1 -1) # 移除索引1和最后一个元素message("List after removing at: ${my_list}") # 输出: List after removing at: apple;cherry
移除重复元素 (REMOVE_DUPLICATES)
移除列表中的所有重复元素,只保留每个元素的第一次出现。
语法
list (REMOVE_DUPLICATES <list>)
示例
set(my_list "apple" "banana" "apple" "cherry" "banana")list(REMOVE_DUPLICATES my_list)message("List after removing duplicates: ${my_list}") # 输出: List after removing duplicates: apple;banana;cherry
列表排序与重排
这类操作会改变列表中元素的顺序。
翻转列表 (REVERSE)
将列表中的元素顺序完全颠倒。
语法
list(REVERSE <list>)
示例
set(my_list "apple" "banana" "cherry")list(REVERSE my_list)message("Reversed list: ${my_list}") # 输出: Reversed list: cherry;banana;apple
排序列表 (SORT)
对列表中的元素进行排序。
语法
list (SORT <list> [COMPARE <compare>] [CASE <case>] [ORDER <order>])
参数说明
COMPARE
:指定排序方法。STRING
(默认): 按字母顺序排序。FILE_BASENAME
:按文件名的基本名称(不含路径)排序。NATURAL
:按自然数顺序排序(例如 "file2.txt" 会排在 "file10.txt" 之前)。
CASE
:指定是否区分大小写。SENSITIVE
(默认): 大小写敏感。INSENSITIVE
:大小写不敏感。
ORDER
:指定排序顺序。ASCENDING
(默认): 升序。DESCENDING
:降序。
示例
set(my_list "c_file" "B_file" "a_file")# 默认排序 (大小写敏感,升序)
set(sorted_list ${my_list})
list(SORT sorted_list)
message("Default sort: ${sorted_list}") # 输出: B_file;a_file;c_file# 大小写不敏感,降序
set(sorted_list_insensitive ${my_list})
list(SORT sorted_list_insensitive CASE INSENSITIVE ORDER DESCENDING)
message("Case-insensitive descending sort: ${sorted_list_insensitive}") # 输出: c_file;B_file;a_file