感谢您的点赞、关注、评论、收藏、是对我最大的认可和支持!❤️
本文为cmake入门的初始篇章只介绍了一些cmake的基础概念,后续还会针对性的深入学习介绍cmake,也可看完本篇文章后通过推荐阅读内的示例教程进一步深入学习,示例教程由浅入深构建cmake 项目非常适合cmake入门学习。
本文档可能包含不准确的术语、措辞或技术细节,旨在为修改cmake项目或构建自己的项目的开发人员提供cmake的简要概述。
关键词
: cmake,学习
声明:
本文作者原创,转载请附上文章出处与本文链接。
LLVM 项目和许多基于 LLVM 构建的核心项目均使用 CMake 构建。
官方 CMake 语言参考可在 cmake-language 手册页和cmake-language 在线文档中找到。
CMake 是一种以自己的语言读取脚本文件的工具,这些脚本文件描述了软件项目的构建方式。当 CMake 评估脚本时,它会构建软件项目的内部表示。脚本完全处理后,如果没有错误,CMake 将生成构建文件来实际构建项目。CMake 支持为各种命令行构建工具以及流行的 IDE 生成构建文件。
当用户运行 CMake 时,它会执行各种检查,类似于 autoconf 过去的工作方式。在检查和评估构建描述脚本期间,CMake 将值缓存到 CMakeCache 中,这很有用,因为它允许构建系统在增量开发期间跳过长时间运行的检查,但CMake 缓存也有一些缺点。
CMake 的脚本语言语法非常简单。每个语言结构都是与模式 _name_(_args_) 匹配的命令。命令主要有三种类型:语言定义(CMake 中以 C++ 实现的命令)、定义函数和定义宏。CMake 发行版还包含一套 CMake 模块,其中包含有用的功能定义。
以下示例是用于构建 C++“Hello World”程序的完整 CMake 构建,该示例仅使用 CMake 语言定义的函数。
cmake_minimum_required(VERSION 3.20.0)
project(HelloWorld)
add_executable(HelloWorld HelloWorld.cpp)
CMake 语言以 foreach 循环和 if 块的形式提供控制流构造。为了使上述示例更加复杂,可以添加 if 块来定义“APPLE”(当针对 Apple 开发平台时):
cmake_minimum_required(VERSION 3.20.0)
project(HelloWorld)
add_executable(HelloWorld HelloWorld.cpp)
if(APPLE)
target_compile_definitions(HelloWorld PUBLIC APPLE)
endif()
在 CMake 中,变量是“字符串”类型。在评估过程中,所有变量都表示为字符串。包装变量会取消引用它,并导致名称对值的文字替换。CMake 在其文档中将此称为“变量评估”。在调用的命令接收参数之前${}
执行取消引用 。这意味着取消引用列表会导致将多个单独的参数传递给命令。
变量取消引用可以嵌套并用于对复杂数据进行建模。例如:
set(var_name var1)
set(${var_name} foo) # same as "set(var1 foo)"
set(${${var_name}}_var bar) # same as "set(foo_var bar)"
取消引用未设置的变量会导致空扩展。CMake 中的一种常见模式是,有条件地设置变量,并知道它将在未设置变量的代码路径中使用。整个 LLVM CMake 构建系统中都有这种示例。
变量空扩展的一个例子是:
if(APPLE)
set(extra_sources Apple.cpp)
endif()
add_executable(HelloWorld HelloWorld.cpp ${extra_sources})
在此示例中,extra_sources
仅当您针对 Apple 平台时才定义变量。对于所有其他目标,extra_sources
在为 add_executable 提供其参数之前,该变量将被评估为空。
在 CMake 中,列表本质是用分号分隔的字符串,因此强烈建议不要在列表中使用分号;
这会不方便定义。定义列表的几个示例:
# Creates a list with members a, b, c, and d
set(my_list a b c d)
set(my_list "a;b;c;d")
# Creates a string "a b c d"
set(my_string "a b c d")
CMake 中比较复杂的模式之一是列表的列表。由于列表不能包含带分号的元素,因此要构造列表的列表,您需要创建一个引用其他列表的变量名称列表。例如:
set(list_of_lists a b c)
set(a 1 2 3)
set(b 4 5 6)
set(c 7 8 9)
使用此布局,您可以用以下代码遍历列表列表并打印每个值:
foreach(list_name IN LISTS list_of_lists)
foreach(value IN LISTS ${list_name})
message(${value})
endforeach()
endforeach()
你会注意到,内层 foreach 循环的列表被双重解除引用。这是因为第一次解除引用变为list_name
子列表的名称(示例中为 a、b 或 c),然后第二次解除引用是获取列表的值。
这种模式在整个 CMake 中使用,最常见的例子是编译器标志选项,CMake 使用以下变量扩展来引用它:CMAKE_${LANGUAGE}_FLAGS
和 CMAKE_${LANGUAGE}_FLAGS_${CMAKE_BUILD_TYPE}
。
缓存或在命令行上指定的变量可以具有与其关联的类型。CMake 的 UI 工具使用变量的类型来显示正确的输入字段。变量的类型通常不会影响评估,但是 CMake 对某些变量(例如 PATH)有特殊处理。您可以在CMake 的 set 文档中阅读有关特殊处理的更多信息。
CMake 本身具有基于目录的作用域。在CMakeLists文件中设置变量,将为该文件和所有子目录设置变量。在CMake模块中设置的变量被包含在CMakeLists文件中,将被设置在它们被包含的范围内,以及所有子目录中。
当在子目录中再次设置已设置的变量时,它将覆盖该范围和任何更深的子目录中的值。
CMake的set 命令提供了两个与作用域相关的选项。PARENT_SCOPE
将变量设置为父作用域,而不是当前作用域。CACHE
选项将变量设置为 CMakeCache 中的变量,从而将其设置为所有作用域中的变量,除非又指定了 FORCE 选项,否则 CACHE 选项不会设置 CACHE 中已存在的变量。
除了基于目录的作用域之外,CMake 函数还有自己的作用域。这意味着函数内部设置的变量不会渗入父作用域。宏则不然,因此只要合理,LLVM 会优先选择函数而不是宏。
与基于 C 的语言不同,CMake 的循环和控制流块没有自己的范围。
CMake 具有与任何脚本语言相同的基本控制流结构,但也存在一些怪癖,因为与 CMake 中的所有内容一样,控制流结构都是命令。
有关 CMake if 命令的完整文档,请点击 此处。该资源更加完整。
一般来说,CMake if 块会按您设定的方式工作:
if(<condition>)
message("do stuff")
elseif(<condition>)
message("do other stuff")
else()
message("do other other stuff")
endif()
对于会 C 语言的人来说,关于 CMake 的 if 块,最重要的一点是它们没有自己的范围,在条件块内设置的变量在之后仍然存在endif()
。
CMakeforeach
块的最常见形式是:
foreach(var ...)
message("do stuff")
endforeach()
块的变量参数部分foreach
可以包含取消引用的列表,要迭代的值或两者的混合:
foreach(var foo bar baz)
message(${var})
endforeach()
# prints:
# foo
# bar
# baz
set(my_list 1 2 3)
foreach(var ${my_list})
message(${var})
endforeach()
# prints:
# 1
# 2
# 3
foreach(var ${my_list} out_of_bounds)
message(${var})
endforeach()
# prints:
# 1
# 2
# 3
# out_of_bounds
还有更现代的 CMake foreach 语法。下面的代码与上面的代码等效:
foreach(var IN ITEMS foo bar baz)
message(${var})
endforeach()
# prints:
# foo
# bar
# baz
set(my_list 1 2 3)
foreach(var IN LISTS my_list)
message(${var})
endforeach()
# prints:
# 1
# 2
# 3
foreach(var IN LISTS my_list ITEMS out_of_bounds)
message(${var})
endforeach()
# prints:
# 1
# 2
# 3
# out_of_bounds
与条件语句类似,它们的行为通常符合您的预期,并且它们没有自己的范围。
CMake 也支持while
循环,尽管它们在 LLVM 中并不广泛使用。
模块是 CMake 实现代码重用的载体。CMake 模块只是 CMake 脚本文件。它们可以包含在 include,模块可添加执行的代码以及命令的定义
。
在 CMake 中,宏和函数被普遍称为命令,它们是定义可多次调用的代码的主要方法。
项目一般从CMakeLists.txt开始编写,但是随着时间的推移,CMakeLists.txt文件会变的越来越大,这个时候我们就需要将CMakeLists.txt文件拆开分写成几个小的模块,这样做的好处是:
1.主文件CMakeLists.txt易读性强。
2.CMake 模块能够在其它项目中重复使用。
3.和CMake的函数相比,模块能够帮助我们限制变量的作用域。
本例中我们将使用两个文件:
CMakeLists.txt 和 cmake/colors.cmake
├── cmake
│ └── colors.cmake
└── CMakeLists.txt 符号
CMakeLists.txt则可以通过下面的指令引用colors.cmake模块文件。
list(APPEND CMAKE_MODULE_PATH “${CMAKE_CURRENT_SOURCE_DIR}/cmake”)
include(colors)
在定义 CMake 命令时,包装处理参数非常有用。本节中的示例使用 CMake function
块,但这也适用于macro
块。
CMake 命令可以有命名参数,这些参数在每个调用站点都是必需的。此外,所有命令都将隐式地接受可变数量的额外参数(在C语言中,所有命令都是变量函数)。当一个命令被调用时带有额外的参数(除了已命名的参数),CMake 将在名为ARGV
的列表中存储完整的参数列表(包括已命名的和未命名的),并在 ARGN
中存储未命名参数的子列表。下面是一个简单的例子,为CMake的内置函数 add_dependencies
提供一个包装函数。
function(add_deps target)
add_dependencies(${target} ${ARGN})
endfunction()
这个例子定义了一个名为 add_deps
的新函数,它接受必需的第一个参数,然后调用另一个函数add_dependencies
,传递第一个参数和所有后面的参数。
CMake 提供了一个模块CMakeParseArguments
,该模块提供了高级参数解析的实现,建议任何具有复杂的基于参数的行为或可选参数的函数都可以使用它。CMake 的模块官方文档位于 cmake-modules
手册页中,也可以在 cmake-modules 在线文档中找到。
从 CMake 3.5 开始,
cmake_parse_arguments
命令替代CMakeParseArguments
的作用并成为一个本地命令,而CMakeParseArguments
模块变为空的,只是为了兼容性而留下。cmake_parse_arguments应用
函数和宏的使用方式非常相似,但两者之间存在一个根本区别,函数有自己的作用域,而宏没有。这意味着宏中设置的变量将渗入调用范围,这使得宏仅适用于定义非常小的功能。
CMake 函数和宏之间的另一个区别是参数的传递方式。宏的参数不设置为变量,而是在执行宏之前在宏中解析对参数的取消引用,如果使用未引用的变量,这可能会导致一些意外行为,例如:
macro(print_list my_list)
foreach(var IN LISTS my_list)
message("${var}")
endforeach()
endmacro()
set(my_list a b c d)
set(my_list_of_numbers 1 2 3 4)
print_list(my_list_of_numbers)
# prints:
# a
# b
# c
# d
一般来说,这个问题并不常见,因为它需要使用名称在父范围内重叠的非解除引用变量,不过还是需要注意,因为它可能导致细微的错误。
CMake 有许多有用的内置命令。本文不会详细介绍它们,因为 CMake 项目有出色的文档。其中包含一些很有用的功能:
需要科学上网的:
end~