黄东旭解析 TiDB 的核心优势
1199
2023-04-22
[GN+Ninja学习 0x03] GN语法与操作学习
想了解更多关于开源的内容,请访问:
开源基础软件社区
OpenHarmony使用gn+ninja来维护开源项目的构建。之前没有接触过gn+ninja,是时候系统性的来学习下了。边学边记录下学习过程,希望对同样需要学习gn+ninja的朋友有所帮助。
1、Design philosophy设计理念
编写构建文件不应该是一项创造性的工作。理想情况下,两个人应该在相同的要求下生成相同的构建文件。除非绝对需要,否则不应该有灵活性。尽可能多的事情应该是致命的错误。构建定义应该读起来更像代码而不是规则。我不想编写或调试Prolog。但是我们团队中的每个人都可以编写和调试C++和Python。构建语言应该对构建应该如何工作持固执己见。表达武断的东西不一定容易,甚至不可能。我们应该改变源代码和工具使构建更简单,而不是使一切都更复杂以符合外部要求(在合理范围内)。在有意义的时候,需要像Blaze一样。
2、Language语言
GN使用机器简单的,动态类型的语言。支持的类型有:
Boolean (true, false). 布尔值。64-bit signed integers. 64位有符号整数。Strings. 字符串。Lists (of any other types). 上述类型的列表。Scopes (sort of like a dictionary, only for built-in stuff). 作用域(类似字典)。
(1)Strings 字符串
字符串括在双引号中,并使用反斜杠作为转义字符。仅仅支持如下转义序列是:
- \"(双引号)- \$(美元符号$)- \\(反斜杠)
反斜杠的任何其他用法都被视为反斜杠。因此,例如,\b不需要转义,大多数 Windows 路径如 "C:\foo\bar.h")不需要转义。
通过符号$支持简单变量替换,其中美元符号$后面的单词被替换为变量的值。如果没有非变量名称字符来终止变量名称,则可以选择${}将名称括起来。不支持更复杂的表达式,仅支持变量名称替换。
a = "mypath"b = "$a/foo.cc" # b -> "mypath/foo.cc"c = "foo${a}bar.cc" # c -> "foomypathbar.cc"
(2)Lists列表
除了把非空列表赋值给空列表(a == [])之外,没有办法获得列表的长度。如果你发现自己想做这种事情,意味着在构建中做太多的工作。— 注:说的是,列表不提供获取长度,也不应该获取长度。
列表追加
列表支持追加,如下所示。将一个列表追加到另一个列表,会把每一个列表项追加为第二个列表中的项,而不是将该列表追加为嵌套成员。
a = [ "first" ]a += [ "second" ] # [ "first", "second" ]a += [ "third", "fourth" ] # [ "first", "second", "third", "fourth" ]b = a + [ "fifth" ] # [ "first", "second", "third", "fourth", "fifth" ]
列表删除
还可以从列表中删除项目,如下。列表中的减号运算符“-”搜索匹配项并删除所有匹配项。从另一个列表中减去一个列表将删除第二个列表中的每个项目。如果未找到匹配的项目,则会引发错误,因此您需要在删除列表项之前,需要提前知道该列表项是否存在。
a = [ "first", "second", "third", "first" ]b = a - [ "first" ] # [ "second", "third" ]a -= [ "second" ] # [ "first", "third", "first" ]
鉴于无法测试列表项的添加引入,可以这样使用:设置一个文件或标志的主列表,然后根据各种条件删除不适用于当前版本的文件或标志。— 注:这算是推荐做法,维护一个主列表,然后只做减法,排除不适合的列表项。这个和下文的GYP提供的建议一样。这里读起来有些奇怪。
在风格上,更喜欢只添加到列表中,让每个源文件或依赖项出现一次。这与Chrome团队过去为GYP提供的建议相反(GYP更愿意列出所有文件,然后基于条件删除您不需要的文件)。
列表项获取
列表支持从零开始的下标来提取值:
a = [ "first", "second", "third" ]b = a[1] # -> "second"
[] 运算符是只读的,不能用于改变列表。其主要使用场景是当外部脚本返回多个已知值,并且您想要提取它们时。
在某些情况下,覆盖一个列表比追加到一个列表更容易。为了帮助满足这种情况,将非空列表赋值给值为非空列表的变量,会产生错误。如果要绕过此限制,请首先将目标变量赋值给一个空列表。如下:
a = [ "one" ]a = [ "two" ] # Error: overwriting nonempty list with a nonempty list.a = [] # OKa = [ "two" ] # OK
(3)Conditionals条件
if (is_linux || (is_win && target_cpu == "x86")) { sources -= [ "something.cc" ] } else if (...) { ... } else { ... }
(4)Looping循环
您可以使用foreach循环访问列表。这是不鼓励的。构建应该做的大多数事情通常都可以在不这样做的情况下来完成,如果你觉得有必要,这可能表明你在元构建中做了太多的工作。
foreach(i, mylist) { print(i) # Note: i is a copy of each element, not a reference to it.}
(5)Function calls函数调用
简单的函数调用看起来像大多数其他语言:
print("hello, world")assert(is_win, "This should only be executed on Windows")
这些函数是内置的,用户无法定义新的函数。一些函数采用以下代码块括起来:{ }。
static_library("mylibrary") { sources = [ "a.cc" ]}
大多数函数定义了目标target。用户可以使用下面讨论的template模板机制定义这样的新功能。
准确地说,上面说的代码块{}作为函数参数来执行函数的。大多数块样式的函数执行代码块,并将生成的作用域做为供读取的变量字典。
(6)Scoping and execution作用域与执行
文件和函数调用后面跟的{}块引入新的作用域。作用域是嵌套的。读取变量时,将按相反的顺序搜索包含作用域,直到找到匹配的名称。变量写入始终转到最内层的作用域。
除了最里面的作用域之外,无法修改任何封闭作用域。这意味着,例如,当您定义target目标时,您在块内执行的任何操作都不会“泄漏”到文件的其余部分。
if/else/foreach语句,即使它们使用{}块,也不会引入新的作用域,因此更改将保留在语句之外。
3、Naming things文件和目录名称
文件名和目录名是字符串,被解释为相对于当前构建文件的目录。有三种可能的形式:
相对名称:
"foo.cc""src/foo.cc""../src/foo.cc"
源树绝对名称:
"//net/foo.cc""//base/test/foo.cc"
系统绝对名称(罕见,通常用于包含目录):
"/usr/local/include/""/C:/Program Files/Windows Kits/Include"
4、Build configuration构建配置
(1)Targets目标
一个目标target是构建图中的一个节点。它通常表示将生成的某种可执行文件或库文件。目标依赖于其他目标。内置目标类型如下所示。可以使用命令gn help
(2)Configs配置
Configs配置是命名对象,用于指定flags、include目录和defines。它们可以应用于目标target并推送到依赖目标。
要定义配置,示例如下:
config("myconfig") { includes = [ "src/include" ] defines = [ "ENABLE_DOOM_MELON" ]}
要将配置应用于目标,可以这样做:
executable("doom_melon") { configs = [ ":myconfig" ]}
(3)Public configs公共配置
一个target目标可以将配置项应用于依赖于它的其他target目标上。最常见的示例是第三方目标,它需要一些定义define或包含头文件的include目录,才能正确编译。您希望这些配置项既应用于第三方库本身的编译,也应用于使用该库的所有目标。
为此,您需要使用要应用的配置项编写一个配置config:
config("my_external_library_config") { includes = "." defines = [ "DISABLE_JANK" ]}
然后,此配置将作为“公共”配置添加到目标中。它将既适用于目标,也适用于直接依赖于它的目标。注:使用的配置项是public_configs。
shared_library("my_external_library") { ... # Targets that depend on this get this config applied. public_configs = [ ":my_external_library_config" ]}
反过来,依赖目标可以通过将目标添加为“公共”依赖项,将其向上推进到依赖项树的另一个级别。注:使用的配置项是public_deps。
static_library("intermediate_library") { ... # Targets that depend on this one also get the configs from "my external library". public_deps = [ ":my_external_library" ]}
(4)Templates模板
模板是 GN 重用代码的主要方式。通常,模板会扩展一个或多个其他target目标类型。
通常,模板定义将放在一个.gni文件中,用户将导入该文件以查看模板定义:
template("idl") { source_set(target_name) { sources = invoker.sources }}
执行模板时的当前工作目录将是调用的构建文件的目录,而不是模板源文件的目录。因此,从模板调用程序传入的文件将是正确的(这通常占模板中的大多数文件处理)。但是,如果模板本身具有文件(也许它会生成运行脚本的操作),则需要使用绝对路径(“//foo/...”)来引用这些文件,以说明当前目录在调用期间是不可预测的。有关详细信息和更完整的示例,请参阅gn help template帮助。
5、Other features其他特性
(1)Imports导入
您可以使用该import函数将.gni文件导入到当前作用域中。这不是C意义上的包含。导入的文件将独立执行,生成的作用域将复制到当前文件中(C当包含指令出现时,在当前上下文中执行包含的文件)。这允许缓存导入的结果,并且还阻止了一些更“创造性”的包含使用,如多次包含的文件。
通常,.gni文件将定义构建参数和模板。有关详细信息,请参阅gn help import。
您的.gni文件可以通过在名称中使用前置下划线(如_this)来表明该临时变量不会到导入文件使用。
(2)Path processing路径处理
通常,您需要相对于其他目录创建文件名或文件名列表。这在运行脚本时尤其常见,脚本是使用构建输出目录作为当前目录执行的,而构建文件通常引用相对于其包含目录的文件。
您可以使用rebase_path转换目录。有关更多帮助和示例,请参阅gn help rebase_path。将相对于当前目录的文件名转换为相对于根构建目录的典型用法是:new_paths = rebase_path("myfile.c", root_build_dir)。
(3)Patterns模式
模式用于为自定义target目标类型的一组给定输入生成输出文件名,并自动从列表值中删除文件(请参见gn help filter_include和gn help filter_exclude)。
它们就像简单的正则表达式。有关详细信息,请参阅gn help label_pattern。
(4)Executing scripts执行脚本
执行脚本的第二种方法是在构建文件执行期间同步执行。在某些情况下,需要确定要编译的文件集,或者获取构建文件可能依赖的某些系统配置。构建文件可以读取脚本的stdout标准输出,并据此以不同的方式对其进行操作。
同步脚本执行由函数exec_script完成(有关详细信息和示例,请参阅gn help exec_script)。由于同步执行脚本需要暂停当前构建文件的执行,直到Python进程完成执行,因此外部脚本的速度很慢,应将其最小化。
为防止滥用,允许调用exec_script的文件可以在顶级.gn文件中列入白名单。更多信息参考gn help dotfile。
您可以同步读取和写入文件,这是不鼓励的,但在同步运行脚本时偶尔是必需的。典型的用例是传递一个长度超过当前平台命令行限制的文件名列表。请参阅gn help read_file和gn help write_file了解如何读取和写入文件。如果可能的话,应避免使用这些功能。
超过命令行长度限制的操作可以使用响应文件来绕过此限制,而无需同步写入文件。查阅gn help response_file_contents了解更多内容。
6、小结
本篇,我们学习了GN语言语法与脚本操作,支持的变量类型,命名、构建配置等等。
想了解更多关于开源的内容,请访问:
开源基础软件社区
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。