Kconfig 宏语言

概念

基本思想来源于 Make。当我们查看 Make 时,我们会注意到它实际上是两种语言的混合。一种语言描述由目标和先决条件组成的依赖关系图。另一种是用于执行文本替换的宏语言。

这两个语言阶段之间有明显的区别。例如,您可以编写如下的 Makefile:

APP := foo
SRC := foo.c
CC := gcc

$(APP): $(SRC)
        $(CC) -o $(APP) $(SRC)

宏语言会将变量引用替换为它们的展开形式,并将处理结果视为如下的源文件输入:

foo: foo.c
        gcc -o foo foo.c

然后,Make 会分析依赖关系图并确定需要更新的目标。

Kconfig 中的概念非常相似 - 可以像这样描述 Kconfig 文件:

CC := gcc

config CC_HAS_FOO
        def_bool $(shell, $(srctree)/scripts/gcc-check-foo.sh $(CC))

Kconfig 中的宏语言会将源文件处理为以下中间状态:

config CC_HAS_FOO
        def_bool y

然后,Kconfig 进入评估阶段,以解决符号间的依赖关系,如 Kconfig 语言 中所述。

变量

与 Make 中一样,Kconfig 中的变量充当宏变量。宏变量会在“原地”展开,生成一个文本字符串,然后可以进一步展开。要获取变量的值,请将变量名括在 $( ) 中。即使是单字母变量名也需要括号;$X 是语法错误。也不支持像 ${CC} 这样的花括号形式。

有两种类型的变量:简单展开变量和递归展开变量。

简单展开变量使用 := 赋值运算符定义。其右侧会在从 Kconfig 文件读取该行时立即展开。

递归展开变量使用 = 赋值运算符定义。其右侧只是作为变量的值存储,而不会以任何方式展开。相反,展开会在使用该变量时执行。

还有另一种类型的赋值运算符;+= 用于向变量追加文本。如果左侧最初定义为简单变量,则 += 的右侧会立即展开。否则,它的评估会延迟。

变量引用可以采用参数,形式如下:

$(name,arg1,arg2,arg3)

您可以将参数化引用视为一个函数。(更准确地说,是“用户定义的函数”,与下面列出的“内置函数”相对)。

有用的函数必须在使用时展开,因为如果传递不同的参数,则同一个函数会以不同的方式展开。因此,用户定义的函数使用 = 赋值运算符定义。参数在主体定义中使用 $(1)、$(2) 等引用。

实际上,递归展开的变量和用户定义的函数在内部是相同的。(换句话说,“变量”是“零参数函数”。)当我们广义地谈论“变量”时,它包括“用户定义的函数”。

内置函数

与 Make 一样,Kconfig 提供了几个内置函数。每个函数都接受特定数量的参数。

在 Make 中,每个内置函数至少接受一个参数。Kconfig 允许内置函数使用零参数,例如 $(filename)、$(lineno)。您可以将它们视为“内置变量”,但这只是我们如何称呼它的问题。让我们在这里称之为“内置函数”,以指代本机支持的功能。

Kconfig 当前支持以下内置函数。

  • $(shell,command)

“shell” 函数接受一个参数,该参数会被展开并传递给子 shell 执行。然后读取命令的标准输出,并将其作为函数的值返回。输出中的每个换行符都会被替换为空格。任何尾随的换行符都会被删除。标准错误不会被返回,任何程序的退出状态也不会被返回。

  • $(info,text)

“info” 函数接受一个参数并将其打印到标准输出。它的计算结果为空字符串。

  • $(warning-if,condition,text)

“warning-if” 函数接受两个参数。如果 condition 部分为 “y”,则 text 部分会发送到标准错误。文本会附加当前 Kconfig 文件名和当前行号作为前缀。

  • $(error-if,condition,text)

“error-if” 函数类似于 “warning-if”,但如果 condition 部分为 “y”,则它会立即终止解析。

  • $(filename)

‘filename’ 不接受参数,$(filename) 会被展开为正在解析的文件名。

  • $(lineno)

‘lineno’ 不接受参数,$(lineno) 会被展开为正在解析的行号。

Make 与 Kconfig

Kconfig 采用类似于 Make 的宏语言,但函数调用语法略有不同。

Make 中的函数调用如下所示:

$(func-name arg1,arg2,arg3)

函数名和第一个参数之间至少由一个空格分隔。然后,会从第一个参数中删除前导空格,而其他参数中的空格则会保留。您需要使用某种技巧才能使第一个参数以空格开头。例如,如果您想让 “info” 函数打印 “ hello”,您可以像下面这样编写:

empty :=
space := $(empty) $(empty)
$(info $(space)$(space)hello)

Kconfig 仅使用逗号作为分隔符,并保留函数调用中的所有空格。有些人喜欢在每个逗号分隔符后放置一个空格:

$(func-name, arg1, arg2, arg3)

在这种情况下,“func-name” 将接收 “ arg1”、“ arg2”、“ arg3”。前导空格的存在与否可能取决于函数。Make 也同样如此 - 例如,$(subst .c, .o, $(sources)) 是一个典型的错误;它会将 “.c” 替换为 “ .o”。

在 Make 中,用户定义的函数通过使用内置函数 ‘call’ 来引用,如下所示:

$(call my-func,arg1,arg2,arg3)

Kconfig 以相同的方式调用用户定义的函数和内置函数。省略 ‘call’ 使语法更简洁。

在 Make 中,某些函数将逗号视为文字,而不是参数分隔符。例如,$(shell echo hello, world) 运行命令 “echo hello, world”。同样,$(info hello, world) 会将 “hello, world” 打印到标准输出。您可以说这是一种 _有用的_ 不一致性。

在 Kconfig 中,为了更简单的实现和语法一致性,出现在 $( ) 上下文中的逗号始终是分隔符。这意味着:

$(shell, echo hello, world)

是一个错误,因为它传递了两个参数,而 ‘shell’ 函数只接受一个参数。要在参数中传递逗号,您可以使用以下技巧:

comma := ,
$(shell, echo hello$(comma) world)

注意事项

变量(或函数)不能跨标记展开。因此,您不能使用变量作为由多个标记组成的表达式的简写。以下代码有效:

RANGE_MIN := 1
RANGE_MAX := 3

config FOO
        int "foo"
        range $(RANGE_MIN) $(RANGE_MAX)

但是,以下代码不起作用:

RANGES := 1 3

config FOO
        int "foo"
        range $(RANGES)

变量不能展开为 Kconfig 中的任何关键字。以下代码不起作用:

MY_TYPE := tristate

config FOO
        $(MY_TYPE) "foo"
        default y

显然,从设计上来说,$(shell command) 在文本替换阶段展开。您不能将符号传递给 ‘shell’ 函数。

以下代码不会按预期工作:

config ENDIAN_FLAG
        string
        default "-mbig-endian" if CPU_BIG_ENDIAN
        default "-mlittle-endian" if CPU_LITTLE_ENDIAN

config CC_HAS_ENDIAN_FLAG
        def_bool $(shell $(srctree)/scripts/gcc-check-flag ENDIAN_FLAG)

相反,您可以像下面这样操作,以便静态展开任何函数调用:

config CC_HAS_ENDIAN_FLAG
        bool
        default $(shell $(srctree)/scripts/gcc-check-flag -mbig-endian) if CPU_BIG_ENDIAN
        default $(shell $(srctree)/scripts/gcc-check-flag -mlittle-endian) if CPU_LITTLE_ENDIAN