# [gn+ninja学习 0x03] gn语法与操作学习

举报
zhushy 发表于 2022/11/09 10:11:46 2022/11/09
【摘要】 [gn+ninja学习 0x03] gn语法与操作学习OpenHarmony使用gn+ninja来维护开源项目的构建。之前没有接触过gn+ninja,是时候系统性的来学习下了。边学边记录下学习过程,希望对同样需要学习gn+ninja的朋友有所帮助。这一篇,我们来学习GN的语法和操作行为等,建议也可以阅读原版文档GN Language and Operation。GN提供了扩展的内置帮助文档...

[gn+ninja学习 0x03] gn语法与操作学习

OpenHarmony使用gn+ninja来维护开源项目的构建。之前没有接触过gn+ninja,是时候系统性的来学习下了。边学边记录下学习过程,希望对同样需要学习gn+ninja的朋友有所帮助。

这一篇,我们来学习GN的语法和操作行为等,建议也可以阅读原版文档GN Language and Operation
GN提供了扩展的内置帮助文档系统,提供每一个函数功能和内置变量的详细的参考引用。可以使用gn help来查看帮助,可以进一步使用gn help <command>gn help <function>gn help <variable>来查看具体的命令、函数、变量的使用帮助信息。

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). 作用域(类似字典)

2.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.2 Lists列表

除了把非空列表赋值给空列表(a == [])之外,没有办法获得列表的长度。如果你发现自己想做这种事情,意味着在构建中做太多的工作。— 注:说的是,列表不提供获取长度,也不应该获取长度。

2.2.1 列表追加

列表支持追加,如下所示。将一个列表追加到另一个列表,会把每一个列表项追加为第二个列表中的项,而不是将该列表追加为嵌套成员。

a = [ "first" ]
a += [ "second" ]  # [ "first", "second" ]
a += [ "third", "fourth" ]  # [ "first", "second", "third", "fourth" ]
b = a + [ "fifth" ]  # [ "first", "second", "third", "fourth", "fifth" ]

2.2.2 列表删除

还可以从列表中删除项目,如下。列表中的减号运算符“-”搜索匹配项并删除所有匹配项。从另一个列表中减去一个列表将删除第二个列表中的每个项目。如果未找到匹配的项目,则会引发错误,因此您需要在删除列表项之前,需要提前知道该列表项是否存在。

a = [ "first", "second", "third", "first" ]
b = a - [ "first" ]  # [ "second", "third" ]
a -= [ "second" ]  # [ "first", "third", "first" ]

鉴于无法测试列表项的添加引入,可以这样使用:设置一个文件或标志的主列表,然后根据各种条件删除不适用于当前版本的文件或标志。— 注:这算是推荐做法,维护一个主列表,然后只做减法,排除不适合的列表项。这个和下文的GYP提供的建议一样。这里读起来有些奇怪。

在风格上,更喜欢只添加到列表中,让每个源文件或依赖项出现一次。这与Chrome团队过去为GYP提供的建议相反(GYP更愿意列出所有文件,然后基于条件删除您不需要的文件)。

2.2.3 列表项获取

列表支持从零开始的下标来提取值:

a = [ "first", "second", "third" ]
b = a[1]  # -> "second"

[] 运算符是只读的,不能用于改变列表。其主要使用场景是当外部脚本返回多个已知值,并且您想要提取它们时。

在某些情况下,覆盖一个列表比追加到一个列表更容易。为了帮助满足这种情况,将非空列表赋值给值为非空列表的变量,会产生错误。如果要绕过此限制,请首先将目标变量赋值给一个空列表。如下:

a = [ "one" ]
a = [ "two" ]  # Error: overwriting nonempty list with a nonempty list.
a = []         # OK
a = [ "two" ]  # OK

2.3 Conditionals条件

条件语句类似于 C语言,如下。可以在大多数情况下,使用条件语句。甚至可以把整个target目标放在条件里,如果这些target只在特定的条件下才需要声明。

  if (is_linux || (is_win && target_cpu == "x86")) {
    sources -= [ "something.cc" ]
  } else if (...) {
    ...
  } else {
    ...
  }

2.4 Looping循环

您可以使用foreach循环访问列表。这是不鼓励的。构建应该做的大多数事情通常都可以在不这样做的情况下来完成,如果你觉得有必要,这可能表明你在元构建中做了太多的工作。

foreach(i, mylist) {
  print(i)  # Note: i is a copy of each element, not a reference to it.
}

2.5 Function calls函数调用

简单的函数调用看起来像大多数其他语言:

print("hello, world")
assert(is_win, "This should only be executed on Windows")

这些函数是内置的,用户无法定义新的函数。一些函数采用以下代码块括起来:{ }

static_library("mylibrary") {
  sources = [ "a.cc" ]
}

大多数函数定义了目标target。用户可以使用下面讨论的template模板机制定义这样的新功能。

准确地说,上面说的代码块{}作为函数参数来执行函数的。大多数块样式的函数执行代码块,并将生成的作用域做为供读取的变量字典。

2.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构建配置

4.1、Targets目标

一个目标target是构建图中的一个节点。它通常表示将生成的某种可执行文件或库文件。目标依赖于其他目标。内置目标类型如下所示。可以使用命令gn help <targettype>以获取更多帮助。可以使用模板创建自定义目标类型,来扩充内置的目标类型。

  • action:运行脚本以生成文件。
  • action_foreach:为每个源文件运行一次脚本。
  • bundle_data:声明数据以进入 Mac/iOS 捆绑包。
  • create_bundle:创建苹果/iOS 捆绑包。
  • executable:生成可执行文件。
  • group:引用一个或多个其他目标的虚拟依赖关系节点。
  • shared_library:共享库.dll或 .so。
  • loadable_module:仅在运行时可加载.dll或 .so。
  • source_set:轻量级虚拟静态库(通常比真正的静态库更可取,因为它的构建速度更快)。
  • static_library:.lib 或 .a 文件(通常可以使用一个source_set替代)。

4.2、Configs配置

Configs配置是命名对象,用于指定flags、include目录和defines。它们可以应用于目标target并推送到依赖目标。

要定义配置,示例如下:

config("myconfig") {
  includes = [ "src/include" ]
  defines = [ "ENABLE_DOOM_MELON" ]
}

要将配置应用于目标,可以这样做:

executable("doom_melon") {
  configs = [ ":myconfig" ]
}

构建配置文件通常会为target目标指定包含默认配置的列表。目标可以根据需要向此列表中添加或删除。因此,在实践中,您通常会使用configs += ":myconfig"附加到默认值列表中。有关如何声明和应用配置的详细信息,请参阅gn help config

4.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.4、Templates模板

模板是 GN 重用代码的主要方式。通常,模板会扩展一个或多个其他target目标类型。

# Declares a script that compiles IDL files to source, and then compiles those
# source files.
template("idl") {
  # Always base helper targets on target_name so they're unique. Target name
  # will be the string passed as the name when the template is invoked.
  idl_target_name = "${target_name}_generate"
  action_foreach(idl_target_name) {
    ...
  }

  # Your template should always define a target with the name target_name.
  # When other targets depend on your template invocation, this will be the
  # destination of that dependency.
  source_set(target_name) {
    ...
    deps = [ ":$idl_target_name" ]  # Require the sources to be compiled.
  }
}

通常,模板定义将放在一个.gni文件中,用户将导入该文件以查看模板定义:

import("//tools/idl_compiler.gni")

idl("my_interfaces") {
  sources = [ "a.idl", "b.idl" ]
}

声明模板会在此Scope作用域内的变量周围创建一个闭包。调用模板时,魔术变量invoker用于读取Scope作用域外的变量。模板通常会将其感兴趣的值复制到自己的Scope作用域内:

template("idl") {
  source_set(target_name) {
    sources = invoker.sources
  }
}

执行模板时的当前工作目录将是调用的构建文件的目录,而不是模板源文件的目录。因此,从模板调用程序传入的文件将是正确的(这通常占模板中的大多数文件处理)。但是,如果模板本身具有文件(也许它会生成运行脚本的操作),则需要使用绝对路径(“//foo/...”)来引用这些文件,以说明当前目录在调用期间是不可预测的。有关详细信息和更完整的示例,请参阅gn help template帮助。

5、Other features其他特性

5.1、 Imports导入

您可以使用该import函数将.gni文件导入到当前作用域中。这不是C++意义上的包含。导入的文件将独立执行,生成的作用域将复制到当前文件中(C++当包含指令出现时,在当前上下文中执行包含的文件)。这允许缓存导入的结果,并且还阻止了一些更“创造性”的包含使用,如多次包含的文件。

通常,.gni文件将定义构建参数和模板。有关详细信息,请参阅gn help import

您的.gni文件可以通过在名称中使用前置下划线(如_this)来表明该临时变量不会到导入文件使用。

5.2、 Path processing路径处理

通常,您需要相对于其他目录创建文件名或文件名列表。这在运行脚本时尤其常见,脚本是使用构建输出目录作为当前目录执行的,而构建文件通常引用相对于其包含目录的文件。

您可以使用rebase_path转换目录。有关更多帮助和示例,请参阅gn help rebase_path。将相对于当前目录的文件名转换为相对于根构建目录的典型用法是:new_paths = rebase_path("myfile.c", root_build_dir)

5.3、Patterns模式

模式用于为自定义target目标类型的一组给定输入生成输出文件名,并自动从列表值中删除文件(请参见gn help filter_includegn help filter_exclude)。

它们就像简单的正则表达式。有关详细信息,请参阅gn help label_pattern

5.4、Executing scripts执行脚本

有两种方法可以执行脚本。GN 中的所有外部脚本都是Python编写的。第一种方法是作为构建步骤。这样的脚本将接受一些输入并生成一些作为构建部分的输出。调用脚本的目标使用“action”目标类型进行声明(请参见gn help action)。

执行脚本的第二种方法是在构建文件执行期间同步执行。在某些情况下,需要确定要编译的文件集,或者获取构建文件可能依赖的某些系统配置。构建文件可以读取脚本的stdout标准输出,并据此以不同的方式对其进行操作。

同步脚本执行由函数exec_script完成(有关详细信息和示例,请参阅gn help exec_script)。由于同步执行脚本需要暂停当前构建文件的执行,直到Python进程完成执行,因此外部脚本的速度很慢,应将其最小化。

为防止滥用,允许调用exec_script的文件可以在顶级.gn文件中列入白名单。更多信息参考gn help dotfile

您可以同步读取和写入文件,这是不鼓励的,但在同步运行脚本时偶尔是必需的。典型的用例是传递一个长度超过当前平台命令行限制的文件名列表。请参阅gn help read_filegn help write_file了解如何读取和写入文件。如果可能的话,应避免使用这些功能。

超过命令行长度限制的操作可以使用响应文件来绕过此限制,而无需同步写入文件。查阅gn help response_file_contents了解更多内容。

6、小结

本篇,我们学习了GN语言语法与脚本操作,支持的变量类型,命名、构建配置等等,可以阅读官方原始文档了解更多信息。匆忙翻译,如有失误,请反馈交流。谢谢。

参考资料

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。