前言
CMake 是个一个开源的跨平台自动化建构系统。CMake 并不直接建构出最终的软件,而是通过 CMakeLists.txt 等输入文件来产生特定平台的标准的构建文件(如 Unix 的 Makefile 或 Windows Visual C++ 的 projects/workspaces),然后再依这些标准的建构文件生成软件。这使得熟悉某个集成开发环境(IDE)的开发者可以用标准的方式建构他的软件。
CMake 语法
注:本章节基于官方文档中 cmake-language 翻译整理而来,旨在提供快速了解 CMake 基本语法的指南,为确保信息的准确性和完整性,请直接阅读原文。
组织
CMake 输入文件(源文件)是以“CMake 语言”编写的CMakeLists.txt文件或以.cmake结尾的文件。
项目中的 CMake 语言源文件组织如下:
CMakeLists.txt当 CMake 处理项目源代码树时,入口点是在顶级源目录中调用的源文件。此文件可能包含整个构建规范,或者使用add_subdirectory()命令将子目录添加到构建中。该命令添加的每个子目录还必须包含一个CMakeLists.txt文件作为该目录的入口点。对于每个CMakeLists.txt处理文件的源目录,CMake 都会在构建树中生成一个相应的目录,作为默认的工作和输出目录。
可以使用带 -P 选项的 cmake(1) 命令行工具以脚本模式处理单个 .cmake 源文件。脚本模式仅运行给定 CMake 语言源文件中的命令,不会生成构建系统。它不允许定义构建目标或操作的 CMake 命令。
目录或脚本中的 CMake 语言代码可以使用 include() 命令在包含上下文的范围内加载 .cmake 源文件。有关 CMake 发行版中包含的模块的文档,请参阅cmake-modules(7) 手册页。项目源树还可以提供自己的模块并在 CMAKE_MODULE_PATH 变量中指定它们的位置。
句法
编码
CMake 语言源文件可以用 7 位 ASCII 文本编写,以实现在所有受支持平台上的最大可移植性。换行符可以编码为 \n 或 \r\n,但在读取输入文件时将转换为 \n。
源文件
cmake_minimum_required(VERSION 3.20) project(hello CXX) if(FALSE AND (1+1>2)) message("满足条件")else() message("不满足条件")endif() # 括号参数message([[This is the first line in a bracket ${variable} argument with bracket length 1]])add_executable(hello hello.cpp)命令
命令调用是一个名称,后跟用空格分隔的括号括起来的参数
add_executable(hello world.c)if(FALSE AND (1+1>2)) #(1+1>2) 执行结果作为参数 message("满足条件")else() message("不满足条件")endif()- 命令参数
# 括号参数message(MAJOR_VERSION:${CMAKE_MAJOR_VERSION}) # MAJOR_VERSION:3message([[MAJOR_VERSION:${CMAKE_MAJOR_VERSION}]]) # MAJOR_VERSION:${CMAKE_MAJOR_VERSION}# 引号参数message("MAJOR_VERSION: ${CMAKE_MAJOR_VERSION}") # MAJOR_VERSION: 3# 非引号参数foreach(arg NoSpace Into;Five;Arguments Escaped\;Semicolon; hh\"hh ) message("${arg}")endforeach()NoSpaceIntoFiveArgumentsEscaped;Semicolonhh"hh变量引用的形式为 ${},在引用参数或非引用参数中进行求值。变量引用会被指定变量或缓存项的值替换,如果两者都未设置,则会被空字符串替换。变量引用可以嵌套,并从内向外求值,例如:${outer_${inner_variable}_variable}
# 变量引用message("CMAKE_MINOR_VERSION:${CMAKE_MINOR_VERSION}")set(HELLO "HELLO")set(HELLO_WROLD "Hello, World!")message("${HELLO}")message("${${HELLO}_WROLD}")CMAKE_MINOR_VERSION:29HELLOHello, World!- 行注释
# This is a line comment \n hh # [].message("First Argument\n" # This is a line comment :) "Second Argument") # This is a line comment.# set(CMAKE_CXX_STANDARD 14)# set(CMAKE_CXX_STANDARD_REQUIRED True)#[[This is a bracket comment.It runs until the close bracket.]]message("First Argument\n" #[[Bracket Comment]] "Second Argument")控制结构
条件块
# 条件语句set(NUM 10) if(NUM LESS 5) message("NUM 小于 5")elseif(NUM EQUAL 5) message("NUM 等于 5")else() message("NUM 大于 5")endif()循环
foreach()/endforeach()和 while()/endwhile()命令界定了要在循环中执行的代码块。在这样的代码块中 break()命令可用于提前终止循环,而continue()命令可用于立即开始下一次迭代。
# 循环语句set(my_list 1 2 3 4 5) foreach(item IN LISTS my_list) if(item STREQUAL "5") message("遇到了5,终止循环") break() endif() if(item STREQUAL "4") message("遇到了${item},跳过这次迭代") continue() endif() message("当前项为: ${item}")endforeach()set(counter 0) while(counter LESS 5) math(EXPR counter "${counter}+1") if(counter EQUAL 3) message("遇到了3,跳过此次迭代") continue() endif() message("当前计数: ${counter}")endwhile()命令定义
macro()/endmacro(), 和 function()/endfunction()命令界定了要记录的代码块,以便以后作为命令调用。
macro(print_message message_text) message("消息内容: ${message_text}")endmacro() # 调用宏print_message("这是一个测试消息")function(add_numbers num1 num2) math(EXPR result "${num1} + ${num2}") message("结果: ${result}")endfunction() # 调用函数add_numbers(5 3)变量
变量是 CMake 语言中的基本存储单位。它们的值始终是字符串类型,尽管某些命令可能会将字符串解释为其他类型的值。 set() 和 unset() 命令显式设置或取消设置变量,但其他命令也具有修改变量的语义。变量名称区分大小写,并且可以包含几乎任何文本,但我们建议坚持仅由字母数字字符加上 _ 和 - 组成的名称。
- 块范围
block() 命令可以为变量绑定创建新的范围。
- 功能范围
由 function() 命令创建的命令定义创建命令,这些命令在调用时会在新的变量绑定范围中处理记录的命令。变量 “set” 或 “unset” 绑定在此作用域中,并且对当前函数及其内的任何嵌套调用可见,但在函数返回后不可见。
- 目录范围
源树中的每个目录都有自己的变量绑定。在处理目录的 CMakeLists.txt 文件之前,CMake 会复制父目录中当前定义的所有变量绑定(如果有),以初始化新的目录范围。当使用 cmake -P 处理时,CMake 脚本会将变量绑定到一个“目录”范围内。
- 持久缓存
CMake 存储一组单独的“缓存”变量或“缓存条目”,其值在项目构建树中的多次运行中保持不变。缓存条目具有仅通过显式请求修改的隔离绑定范围,例如通过 set() 和 unset() 命令的 CACHE 选项。
在评估变量引用时,CMake 首先在函数调用堆栈(如果有)中搜索绑定,然后回退到当前目录范围中的绑定(如果有)。如果找到 “set” 绑定,则使用其值。如果找到 “未设置” 绑定,或者未找到绑定,CMake 将搜索缓存条目。如果找到缓存条目,则使用其值。否则,变量引用的计算结果为空字符串。 $CACHE{VAR} 语法可用于直接查找缓存条目。
cmake-variables(7) 手册记录了 CMake 提供的许多变量,或者在由项目代码设置时对 CMake 有意义的变量。
CMake 预设了一些常用变量,这些变量通常会在编写 CMakeLists.txt 文件时使用到,这些变量可在cmake-variables(7) 手册中进行查询。
CMAKE_MAJOR_VERSION:cmake 主版本号CMAKE_MINOR_VERSION:cmake 次版本号CMAKE_C_FLAGS:设置 C 编译选项CMAKE_CXX_FLAGS:设置 C++ 编译选项PROJECT_SOURCE_DIR:工程的根目录PROJECT_BINARY_DIR:运行 cmake 命令的目录CMAKE_CURRENT_SOURCE_DIR:当前 CMakeLists.txt 所在路径CMAKE_CURRENT_BINARY_DIR:目标文件编译目录EXECUTABLE_OUTPUT_PATH:重新定义目标二进制可执行文件的存放位置LIBRARY_OUTPUT_PATH:重新定义目标链接库文件的存放位置UNIX:如果为真,表示为UNIX-like的系统,包括AppleOSX和CygWinWIN32:如果为真,表示为 Windows 系统,包括 CygWinAPPLE:如果为真,表示为 Apple 系统CMAKE_SIZEOF_VOID_P:表示void*的大小(例如为4或者8),可以使用其来判断当前构建为32位还是64位CMAKE_CURRENT_LIST_DIR:表示正在处理的CMakeLists.txt文件所在目录的绝对路径CMAKE_ARCHIVE_OUTPUT_DIRECTORY:用于设置ARCHIVE目标的输出路径CMAKE_LIBRARY_OUTPUT_DIRECTORY:用于设置LIBRARY目标的输出路径CMAKE_RUNTIME_OUTPUT_DIRECTORY:用于设置RUNTIME目标的输出路径环境变量
环境变量与普通变量类似,但有以下区别:
- 范围
环境变量在 CMake 进程中具有全局范围。它们永远不会被缓存。
- 参考
变量引用的形式为 $ENV{},使用 ENV 运算符。
- 初始化
CMake 环境变量的初始值是调用进程的初始值。可以使用 set() 和 unset() 命令更改值。这些命令仅影响正在运行的 CMake 进程,而不影响整个系统环境。更改的值不会写回调用进程,并且后续构建或测试进程不会看到它们。 请参阅 cmake -E env 命令行工具以在修改后的环境中运行命令。
- 检查
查看cmake -E环境命令行工具显示当前所有环境变量。
cmake-env-variables(7) 手册记录了对 CMake 有特殊含义的环境变量。
列表
在CMake中,你可以使用列表 list 来存储和操作一组数据。
# 创建一个包含字符串的列表set(my_list "apple" "banana" "cherry") # 遍历列表中的元素并输出foreach(item IN LISTS my_list) message("Item: ${item}")endforeach() # 向列表添加元素list(APPEND my_list "date") # 输出更新后的列表message("Updated List: ${my_list}") # 获取列表长度list(LENGTH my_list list_length)message("List Length: ${list_length}") # 获取列表指定索引处的元素list(GET my_list 1 element)message("Element at index 1: ${element}") # 删除列表中的元素list(REMOVE_ITEM my_list "banana") # 输出更新后的列表message("List after removing 'banana': ${my_list}")尽管 CMake 中的所有值都存储为字符串,但在某些上下文中,例如在评估非引号参数期间,字符串可能会被视为列表,在这种情况下,通过;将字符串拆分为列表元素。
# set() 命令将多个值作为列表存储到目标变量中set(srcs a.c b.c c.c)message("srcs: ${srcs}") set(x a "b;c")message("x: ${x}")srcs: a.c;b.c;c.cx: a;b;cCMake 命令可在 CMake 开发文档中查看,使用搜索可快速找到自己要使用的指令。
注:本章节基于官方文档中 cmake-commands 部分常用命令整理而来,为确保信息的准确性和完整性,请直接阅读原文。
脚本命令
将普通变量、缓存变量或环境变量设置为给定值。
- 普通变量
- 缓存变量:设置一个变量,并将其缓存起来,以便在后续的CMake运行中保留该变量的值
set( ... CACHE [FORCE])
- : 要设置的变量的名称。
- ...: 变量的值。可以是一个或多个值,用空格分隔。如果只有一个值,可以直接写在位置;如果有多个值,可以列出多个值。
- CACHE : 将变量缓存起来,以便在后续的CMake运行中保留该变量的值。指定了变量的类型,可以是以下几种之一:
- BOOL: 布尔类型。
- STRING: 字符串类型。
- FILEPATH: 文件路径类型。
- PATH: 路径类型。
- INTERNAL: 内部类型,不会显示在CMake GUI中,用于内部使用。
- : 用于描述这个变量的文档字符串,会在CMake GUI中显示。可以提供有关变量用途和说明的文本。
- [FORCE]: 可选参数,用于强制设置变量的值,即使变量之前已经被设置过。如果使用了FORCE选项,将会覆盖之前的值。
- 环境变量:将环境变量设置为给定值。调用 $ENV{} 取值。
set(ENV{} [])
- ENV{}: 这部分用于指定要设置的环境变量的名称。在ENV{}中使用表示环境变量的名称。
- []: 这部分是可选的,用于指定要为环境变量设置的值。如果提供了,则会将环境变量设置为指定的值;如果没有提供,则会将环境变量设置为空。
取消设置变量、缓存变量或环境变量。
- 取消设置普通变量或缓存变量
- 取消设置环境变量:此命令仅影响当前的 CMake 进程,而不影响调用 CMake 的进程,也不会影响整个系统环境,也不会影响后续构建或测试进程的环境
unset(ENV{})
删除Environment Variables。后续调用$ENV{}将返回空字符串
记录一条消息。
- 一般消息
有条件地执行一组命令。
概要
if(<condition>) <commands>elseif(<condition>)# optional block, can be repeated <commands>else()# optional block <commands>endif()根据条件语法评估子句condition的参数。如果结果为真,则 执行块中的。否则,以相同方式处理可选块。最后,如果没有为真, 则执行可选块中的else 。
对以分号分隔的列表进行的操作。
概要
Reading list(LENGTH <list> <out-var>) list(GET <list> <element index> [<index> ...] <out-var>) list(JOIN <list> <glue> <out-var>) list(SUBLIST <list> <begin> <length> <out-var>) Search list(FIND <list> <value> <out-var>) Modification list(APPEND <list> [<element>...]) list(FILTER <list> {INCLUDE | EXCLUDE} REGEX <regex>) list(INSERT <list> <index> [<element>...]) list(POP_BACK <list> [<out-var>...]) list(POP_FRONT <list> [<out-var>...]) list(PREPEND <list> [<element>...]) list(REMOVE_ITEM <list> <value>...) list(REMOVE_AT <list> <index>...) list(REMOVE_DUPLICATES <list>) list(TRANSFORM <list> <ACTION> [...]) Ordering list(REVERSE <list>) list(SORT <list> [...])定义函数以作为命令调用。
函数一般定义形式如下
function(<name> [<arg1> ...]) <commands>endfunction()定义一个名为 的函数,它接受名为 , ... 的参数,调用函数执行 语句;在调用该函数之前它们不会被执行。
文件操作命令。该命令专用于需要访问文件系统的文件和路径操作。对于其他路径操作,仅处理语法方面,请查看 cmake_path()命令。
概要
Reading file(READ <filename> <out-var> [...]) file(STRINGS <filename> <out-var> [...]) file(<HASH> <filename> <out-var>) file(TIMESTAMP <filename> <out-var> [...]) file(GET_RUNTIME_DEPENDENCIES [...]) Writing file({WRITE | APPEND} <filename> <content>...) file({TOUCH | TOUCH_NOCREATE} <file>...) file(GENERATE OUTPUT <output-file> [...]) file(CONFIGURE OUTPUT <output-file> CONTENT <content> [...]) Filesystem file({GLOB | GLOB_RECURSE} <out-var> [...] <globbing-expr>...) file(MAKE_DIRECTORY <directories>...) file({REMOVE | REMOVE_RECURSE } <files>...) file(RENAME <oldname> <newname> [...]) file(COPY_FILE <oldname> <newname> [...]) file({COPY | INSTALL} <file>... DESTINATION <dir> [...]) file(SIZE <filename> <out-var>) file(READ_SYMLINK <linkname> <out-var>) file(CREATE_LINK <original> <linkname> [...]) file(CHMOD <files>... <directories>... PERMISSIONS <permissions>... [...]) file(CHMOD_RECURSE <files>... <directories>... PERMISSIONS <permissions>... [...]) Path Conversion file(REAL_PATH <path> <out-var> [BASE_DIRECTORY <dir>] [EXPAND_TILDE]) file(RELATIVE_PATH <out-var> <directory> <file>) file({TO_CMAKE_PATH | TO_NATIVE_PATH} <path> <out-var>) Transfer file(DOWNLOAD <url> [<file>] [...]) file(UPLOAD <file> <url> [...]) Locking file(LOCK <path> [...]) Archiving file(ARCHIVE_CREATE OUTPUT <archive> PATHS <paths>... [...]) file(ARCHIVE_EXTRACT INPUT <archive> [...])提供用户使用的布尔选项,可以用于控制编译流程,类似于 C 语言中的宏条件编译。
option( "" [value])
- :选项的名称。
- :对选项的描述、解释或备注。
- [value]:选项的初始化值(除了 ON 之外,其他值都视为 OFF),OFF 为默认。
在构建过程中执行文件转换。它将一个输入文件复制到一个输出文件,并在复制过程中替换变量和执行其他修改。
折叠内容
configure_file( [NO_SOURCE_PERMISSIONS | USE_SOURCE_PERMISSIONS | FILE_PERMISSIONS ...] [COPYONLY] [ESCAPE_QUOTES] [@ONLY] [NEWLINE_STYLE [UNIX|DOS|WIN32|LF|CRLF] ])
- :输入文件的路径。如果是相对路径,它相对于 CMAKE_CURRENT_SOURCE_DIR 处理。
- :输出文件或目录的路径。如果指定的是现有目录,输出文件将与输入文件同名放置在该目录中。如果路径中包含不存在的目录,它们会被创建。
- 选项:
- NO_SOURCE_PERMISSIONS:从输入文件不传递权限到输出文件。复制的文件权限默认为标准的 644 值(-rw-r–r–)。
- USE_SOURCE_PERMISSIONS:将输入文件的权限传递给输出文件。如果没有给出 NO_SOURCE_PERMISSIONS、USE_SOURCE_PERMISSIONS 或 FILE_PERMISSIONS 中的任何一个关键字,这已经是默认行为。
- FILE_PERMISSIONS ...:忽略输入文件的权限,使用指定的 替代输出文件的权限。
- COPYONLY:仅复制文件,不替换任何变量引用或其他内容。此选项不能与 NEWLINE_STYLE 一起使用。
- ESCAPE_QUOTES:使用反斜杠(类似 C 语言)转义任何替换的引号。
- @ONLY:仅限于替换形式为 @VAR@ 的变量引用。这对于配置使用 ${VAR} 语法的脚本非常有用。
- NEWLINE_STYLE :指定输出文件的换行符样式。使用 UNIX 或 LF 表示 \n 换行,使用 DOS、WIN32 或 CRLF 表示 \r\n 换行。此选项不能与 COPYONLY 一起使用。
## 项目命令
指定最低版本的 cmake。
cmake_minimum_required(VERSION [...] [FATAL_ERROR])
- :指定项目所需的最低 CMake 版本。
- [...]:可选部分,用于指定一些策略的最大版本。
- [FATAL_ERROR]:可选部分,如果指定了此参数并且 CMake 版本低于指定的最低版本,则会产生致命错误并终止 CMake 运行。
设置项目名称。
project( [...])
- :指定项目的名称。
- [...]:可选部分,用于指定项目中使用的编程语言。这个参数可以是一个或多个语言名称,例如 C、CXX(C++)、Fortran 等。
折叠内容
project( [VERSION [.[.[.]]]] [DESCRIPTION ] [HOMEPAGE_URL ] [LANGUAGES ...])
- :这是你的项目的名称。这个名称将被用作许多内置变量的基础。
- VERSION [.[.[.]]]:这是一个可选参数,用于指定项目的版本号。你可以指定主版本号,次版本号,补丁版本号,以及微调版本号。
- DESCRIPTION :这是一个可选参数,用于提供项目的描述。
- HOMEPAGE_URL :这是一个可选参数,用于指定项目主页的URL。
- LANGUAGES ...:这是一个可选参数,用于指定项目中使用的编程语言。如果省略,CMake将默认启用C和C++。
使用指定的源文件编译可执行文件。
add_executable( ... ...)
- :指定要创建的可执行文件的名称。
- ...:可选项,用于指定与可执行文件相关的选项,例如编译标志等。
- ...:指定用于构建可执行文件的源文件列表。
示例如下
// 创建一个名为 my_app 的可执行文件,使用 main.cpp 和 helper.cpp 作为源文件来构建它add_executable(my_app main.cpp helper.cpp)将子目录添加到构建中。
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])
- source_dir:这是你想要添加的源目录的路径。这个目录应该包含一个CMakeLists.txt文件。
- binary_dir:这是一个可选参数,用于指定二进制文件的输出目录。如果省略,CMake将使用source_dir相对于当前源目录的相对路径。
- EXCLUDE_FROM_ALL:这是一个可选参数。如果指定,那么这个子目录将不会被all目标包含,也就是说,当你运行make all或者直接运行make时,这个子目录不会被构建。
- SYSTEM:这是一个可选参数。如果指定,那么这个子目录中的目标将被视为系统目标。这意味着,对于这个目录中的目标,任何与其相关的头文件目录都会被编译器视为系统头文件目录。
指定一个目标需要链接的库。
折叠内容
target_link_libraries( ... [ ...]...)
- :这是你想要链接库的目标。它可以是任何已经被CMake识别的目标,例如一个可执行文件,库,或者是另一个目标。
- ...:这些参数用于指定链接的库以及链接的类型。是你想要链接的库的名称。链接类型可以是PRIVATE,PUBLIC,或者INTERFACE:
- PRIVATE:库只会被目标自身使用,不会被链接到目标的消费者。
- PUBLIC:库既会被目标自身使用,也会被链接到目标的消费者。
- INTERFACE:库不会被目标自身使用,但会被链接到目标的消费者。
为特定目标添加头文件搜索路径。
折叠内容
target_include_directories( [SYSTEM] [AFTER|BEFORE] [items1...] [ [items2...] ...])
- :目标的名称(使用 add_executable() 或 add_library() 创建的目标)。
- [SYSTEM]:可选参数。如果指定,目录将被视为系统头文件目录(用于系统头文件)。
- [AFTER|BEFORE]:可选参数。确定目录是追加还是插入到现有的头文件搜索路径中。
- :指定后续参数的作用域。
- PRIVATE 和 PUBLIC 会影响目标本身。
- INTERFACE 影响链接到该目标的其他目标。
- [items1...]:要添加的头文件搜索路径。
使用指定的源文件作为库添加到项目中。
- 普通库
add_library( [] [EXCLUDE_FROM_ALL] ...)
- :库的名称,必须在整个项目中是全局唯一的。
- :可选参数,指定库的类型。
- STATIC:静态库,用于链接到其他目标。
- SHARED:动态库,可以在运行时被其他目标链接。
- MODULE:插件模块,不会被其他目标链接,但可以在运行时使用类似 dlopen 的功能动态加载。
- 如果不指定 ,默认为 STATIC 或 SHARED,取决于 BUILD_SHARED_LIBS 变量的值。
- [EXCLUDE_FROM_ALL]:可选参数,如果指定,该库不会被默认构建,除非其他目标显式地依赖于它。
- :源文件列表。
- 对象库:这种类型的库只编译源文件,不会将生成的目标文件打包或链接为库。其他由 add_library 或 add_executable 创建的目标可以使用表达式 $ 引用这些对象。
add_library( OBJECT ...)
- : 对象库的名称,用于在项目中标识这个对象库。
- OBJECT: 指定这是一个对象库,而不是静态库或共享库。
- ...: 用于构建对象库的源文件列表。
- 接口库:与普通库不同,接口库不会编译任何源文件,也不会在磁盘上生成库文件。它主要用于定义其他目标的使用要求,而不产生实际的库文件。
add_library( INTERFACE)
- :这是你想要创建的库的名称。这个名称将被用作许多内置变量的基础。
- INTERFACE:这个关键字指定了库的类型为接口库。接口库不直接构建任何二进制文件,而是用于收集和传递使用需求,例如编译器标志,包含目录,链接库等。这些需求可以通过target_link_libraries命令传递给其他目标。
将包含目录添加到构建中。
为指定目标文件添加编译选项。
折叠内容
target_compile_definitions( [items1...] [ [items2...] ...])
- :目标文件的名称,可以是可执行文件或库文件。
- :用于指定编译选项的作用域。
- INTERFACE:选项将应用于目标的接口(用于链接到目标的其他目标)。
- PUBLIC:选项将应用于目标本身和链接到它的其他目标。
- PRIVATE:选项仅应用于目标本身。
- [items1...]:要添加的宏定义。
CMake 常用项目配置方式
下面使用一个简单的示例程序,来演示 CMake 配置项目的几种常见方式。示例程序的文件和目录结构如下
// 目录结构.├── CMakeLists.txt├── hello.cpp├── include│ └── log.h└── src └── log.cpp // hello.cpp 文件 #include <iostream>#include "include/log.h" int main(){ std::cout << "hello world" << std::endl; log("This is a log message.");} // log.h 文件#include <iostream>#include <ctime> using namespace std;void log(const string& message); // log.cpp 文件#include "../include/log.h" void log(const string& message) { // 获取当前日期和时间 time_t now = time(0); struct tm *localTime = localtime(&now); // 格式化日期和时间 char buffer[80]; strftime(buffer, 80, "%Y-%m-%d %H:%M:%S", localTime); // 输出日志信息 cout << "[" << buffer << "] " << message << endl;}相对路径方式
使用相对路径的方式配置项目,log.cpp、hello.cpp 引入头文件时需保证头文件路径正确。常见于一些演示 demo。
调整目录结构如下
// 目录结构.├── CMakeLists.txt├── hello.cpp└── log ├── include │ └── log.h ├── log.cmake └── src └── log.cpplog 目录下添加 log.cmake 配置文件
// log.cmake 文件// 参与编译的源文件设置给 log_sources 变量set(log_sources log/src/log.cpp)CMakeLists.txt 嵌套方式
目录结构如下
.├── CMakeLists.txt├── hello.cpp└── log ├── CMakeLists.txt ├── include │ └── log.h └── src └── log.cpplog 目录下添加 CMakeLists.txt 配置文件
# 指定源文件,创建 log_lig 库add_library(log_lib src/log.cpp)在工程根目录下的 CMakeLists.txt 文件里,引入 log_lig库
Object Libraries 方式
使用 add_library( OBJECT ...) 命令。Object Library (最低版本 3.12)是一个特殊的库类型,它将目标文件编译成一个库,但不会生成最终的链接文件,这意味着你可以在后续的 add_library 或 add_executable 命令中,将 Object Library 作为源文件进行链接,从而生成最终的可执行文件或库文件。
目录结构如下
.├── CMakeLists.txt├── hello.cpp└── log ├── CMakeLists.txt ├── include │ └── log.h └── src └── log.cpplog 目录下添加 CMakeLists.txt 配置文件
# 对象库只编译源文件,不会将生成的目标文件打包或链接为库add_library(log_lib OBJECT src/log.cpp) target_include_directories(log_lib PUBLIC include)在工程根目录下的 CMakeLists.txt 文件里,将 Object Library 作为源文件进行链接,可以看到项目编译后并没有生成 .a 静态库
CMake 静态库与动态链接库
C++ 中的静态库与动态库
在C++编程中,静态库和动态库是常用的库文件类型,用于组织和共享代码。这里是它们的简要介绍
静态库(Static Library):静态库是在编译时与可执行文件链接的库。它包含了一组预编译的目标文件,这些目标文件在链接时会被整合到可执行文件中。
lib.a/lib
- 特点:
- 静态库的代码会被复制到可执行文件中,使得可执行文件变得独立。
- 静态库的大小会增加可执行文件的大小。
- 静态库在链接时被静态地链接到可执行文件中。
- 优点:
- 静态库的部署简单,不需要额外的库文件。
- 可以避免动态库版本兼容性的问题。
动态库(Dynamic Library):动态库是在运行时加载到内存中的库。它包含了可执行代码,但在程序启动时并不被复制到可执行文件中。
lib.so/dll 文件
- 特点:
- 动态库在运行时被加载到内存中,并可以被多个程序共享。
- 多个程序可以共享同一个动态库的实例。
- 动态库的更新会影响所有使用它的程序。
- 优点:
- 节省内存,因为多个程序可以共享同一个动态库的实例。
- 可以通过更新动态库来修复 bug 或增加功能,而无需重新编译可执行文件。
调用库
静态库调用方式
- 引入头文件
- 链接静态库
- 生成可执行二进制文件
动态库调用方式
- 引入头文件
- 声明库目录
- 生成可执行二进制文件
- 链接动态库
本节通过一个简单的示例,演示如何在 CMake 工程中添加交叉编译的配置,实现编译不同硬件平台的可执行文件。
准备交叉编译工具
确保目标系统的交叉编译工具已经下载配置完成。本示例是在 macOS 系统下编译 windows 系统的可执行文件,所以下载 mingw-w64 编译工具。
brew install mingw-w64查看安装目录下面的 g++ 编译器,验证安装是否成功
~ /opt/homebrew/Cellar/mingw-w64/11.0.1_1/toolchain-x86_64/bin/x86_64-w64-mingw32-g++ --versionx86_64-w64-mingw32-g++ (GCC) 13.2.0Copyright (C) 2023 Free Software Foundation, Inc.This is free software; see the source for copying conditions. There is NOwarranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. ➜ ~ 交叉编译配置文件
# tool_chain_windows.cmake 交叉编译配置文件set(CMAKE_SYSTEM_NAME Windows)set(CMAKE_SYSTEM_PROCESSOR x86-64) set(tools /opt/homebrew/Cellar/mingw-w64/11.0.1_1/toolchain-x86_64)set(CMAKE_C_COMPILER ${tools}/bin/x86_64-w64-mingw32-gcc)set(CMAKE_CXX_COMPILER ${tools}/bin/x86_64-w64-mingw32-g++) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)通过 CMake 的变量 CMAKE_TOOLCHAIN_FILE 来指定工具链文件
cmake -DCMAKE_TOOLCHAIN_FILE=tool_chain_windows.cmake编译验证
指定工具链文件后进行编译
对编译输出的 hello.exe 文件,可在 windows 系统下运行进行验证。
与源文件交互
使用 configure_file() 命令,在构建过程中执行文件转换。
示例代码
定义一个输入文件 config.in ,在输入文件内容中引用的变量(如 @VAR@、${VAR}、$CACHE{VAR})将被替换为变量的当前值,如果变量未定义,则替换为空字符串
#define CMAKE_CXX_STANDARD ${CMAKE_CXX_STANDARD}使用 configure_file() 命令,执行文件转换
cmake_minimum_required(VERSION 3.20) project(hello CXX) set(CMAKE_CXX_STANDARD 11)set(CMAKE_CXX_STANDARD_REQUIRED True) # 构建过程中执行文件转换,输入输出文件都在当前目录下,输出 .h 文件,后续可以在 c++ 代码中 includeconfigure_file(config.in config.h) add_executable(hello hello.cpp src/log.cpp)# 添加头文件搜索路径——包含 config.h 生成文件的路径target_include_directories(hello PUBLIC "${PROJECT_BINARY_DIR}")在 c++ 代码中引入头文件,并使用其中宏定义的常量
#include <iostream>#include "include/log.h"#include "config.h" int main(){ std::cout << "hello world" << std::endl; std::cout << CMAKE_CXX_STANDARD << std::endl;}CMake 项目中,可通过这种形式,把 CMake 使用的变量传递给 C++ 代码。
条件编译
在 CMake 中使用选项option()命令,可以控制编译流程。
CMake 输入文件中使用条件编译
使用 option()命令控制 CMake 执行不同的命令
# 添加选项 我们定义了一个名为 ENABLE_DEBUG 的选项,它默认为 ON。你可以根据需要修改选项的值,然后在 CMake 构建过程中使用这些选项来控制不同的功能模块option(ENABLE_DEBUG "Enable Debug Mode" ON)option(LOG_MSG_START_WITH_PREFIX "Log message start with ======> prefix" ON) message("LOG_MSG_START_WITH_PREFIX defined: " ${LOG_MSG_START_WITH_PREFIX})# 模拟使用选项来控制不同的功能模块if (ENABLE_DEBUG) message("ENABLE_DEBUG defined: " ${ENABLE_DEBUG})else () message("ENABLE_DEBUG un-defined: " ${ENABLE_DEBUG})endif() add_library(log_lib src/log.cpp) target_compile_definitions(log_lib PRIVATE "ENABLE_DEBUG" PRIVATE "LOG_MSG_START_WITH_PREFIX") 输出
LOG_MSG_START_WITH_PREFIX defined: ONENABLE_DEBUG defined: ONC++ 代码中使用条件编译
使用 target_compile_definitions()命令,为目标库添加编译选项,在代码中使用这些选项,类似于 C 语言中的宏条件。
#include "../include/log.h" void log(const string &message){// 条件预处理指令,用于检查某个标识符是否已定义#ifdef ENABLE_DEBUG // 获取当前日期和时间 time_t now = time(0); struct tm *localTime = localtime(&now); // 格式化日期和时间 char buffer[80]; strftime(buffer, 80, "%Y-%m-%d %H:%M:%S", localTime); // 输出日志信息 #ifdef LOG_MSG_START_WITH_PREFIX cout << "======> " << "[" << buffer << "] " << message << endl; #else cout << "[" << buffer << "] " << message << endl; #endif#else #endif}