4、配方文件核心语法:变量赋值、Python函数嵌入、inherit与include机制
说实话,Yocto的配方文件(.bb)看起来就是个简单的文本文件,但里面的门道可不少。我刚开始接触时也觉得不就是赋值嘛,结果被各种等号搞晕过好几次。今天咱们就把这块彻底捋清楚。
4.1 变量赋值的五种姿势
Yocto里变量赋值有五种方式,每种都有自己的脾气。我习惯把它们分成两类:硬赋值和软赋值。
4.1.1 硬赋值:=
这个最简单,就是直接赋值。不管之前这个变量是什么值,用了等号就全被覆盖了。
SRC_URI = "https://example.com/myapp-1.0.tar.gz"
嗯,这里要注意:如果你在多个地方对同一个变量用=,后面的会覆盖前面的。我在项目中就踩过这个坑——在配方文件底部又写了个=赋值,结果前面的配置全白写了。
4.1.2 软赋值:?=
?=的意思是「如果变量没定义,我才赋值」。说白了就是有默认值的效果。
COMPATIBLE_MACHINE ?= "generic-arm"
这个在写通用配方时特别有用。你想想看,如果某个BSP层已经定义了COMPATIBLE_MACHINE,你的?=就不会覆盖它。我曾经用这个机制做了一个基础配方,上层只需要在.bbappend里用=覆盖就行了,非常灵活。
4.1.3 弱赋值:??=
??=比?=更「弱」。它会在所有?=赋值之后才生效。也就是说,如果前面有人用了?=,那??=就不会生效了。
PREFERRED_VERSION ??= "1.0"
这个我一般用在全局的.conf文件里,作为最后的兜底方案。举个例子:
| 赋值方式 | 优先级 | 典型场景 |
|---|---|---|
| = | 最高 | 强制覆盖 |
| ?= | 中等 | 提供默认值 |
| ??= | 最低 | 全局兜底 |
4.1.4 追加赋值:+= 和 .=
+=是在变量后面加内容,中间带空格。.=是直接拼接,不带空格。
DEPENDS += "openssl" # 变成 "原有内容 openssl"
DEPENDS .= "openssl" # 变成 "原有内容openssl"
我个人习惯用+=,因为带空格更安全。有一次我用.=拼接路径,结果忘了加斜杠,编译时路径就错了。嗯,从那以后我每次用.=都会多检查一遍。
4.2 Python函数嵌入
Yocto的配方文件里可以直接写Python代码,这个功能太强了。我经常用它来做一些动态计算。
4.2.1 匿名Python函数
用python () { ... }定义的函数会在配方解析时自动执行。
python () {
# 根据机器类型动态设置版本号
if d.getVar('MACHINE') == 'qemuarm':
d.setVar('PV', '1.0-arm')
else:
d.setVar('PV', '1.0-generic')
}
这里要注意:匿名函数没有名字,你没法手动调用它。它就像个初始化脚本,在配方加载时跑一遍。
4.2.2 具名Python函数
用def定义的函数可以被其他任务调用。
def get_version(d):
machine = d.getVar('MACHINE')
if 'arm' in machine:
return '1.0-arm'
return '1.0-generic'
PV = "${@get_version(d)}"
这个${@...}语法是调用Python函数的入口。我刚开始总忘记写@符号,结果变量一直不展开,排查了半天才发现。
bb.note()打印调试信息,比用print()更规范。我在调试复杂配方时经常这么干。
4.3 inherit与include机制
这两个机制是Yocto实现代码复用的核心。说白了就是「抄作业」——把别人写好的功能拿过来用。
4.3.1 inherit:继承类
inherit会引入一个.bbclass文件。这个文件里定义了一组变量和任务,可以理解成面向对象里的「类继承」。
inherit autotools
inherit pkgconfig
我举个例子:你写一个C语言项目,用inherit autotools之后,do_configure、do_compile、do_install这些任务就自动有了。你只需要提供configure.ac和Makefile.am就行。
我曾经接手一个项目,里面inherit了十几个类,结果变量冲突了。排查下来发现两个类都定义了do_install,后面的把前面的覆盖了。所以我的建议是:inherit的类越少越好,够用就行。
4.3.2 include:包含文件
include就是简单的文件包含,类似C语言的#include。它不会做任何特殊处理,就是把文件内容原封不动地贴进来。
include recipes-core/images/core-image-minimal.inc
这里有个坑:include如果文件不存在,不会报错,只是静默跳过。而require如果文件不存在,会直接报错终止。
| 指令 | 文件不存在时 | 典型用途 |
|---|---|---|
| inherit | 报错 | 引入类文件(.bbclass) |
| include | 静默跳过 | 可选配置 |
| require | 报错 | 必需配置 |
require代替include。因为文件缺失时立即报错,比静默跳过更容易发现问题。有一次我写了个include,文件名拼错了,结果编译出来的镜像少了关键功能,排查了整整一天。
4.4 知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以看到变量赋值、Python函数、继承机制这三块是怎么配合的。
你看这张图,左边是变量赋值,中间是Python函数,右边是继承机制。这三块是互相配合的。比如你可以在Python函数里用d.getVar()读取变量,也可以用d.setVar()修改变量。而inherit进来的类文件里,也可能定义了变量和Python函数。
我个人觉得,理解这三块的核心在于:变量是数据,Python函数是逻辑,inherit/include是组织方式。把这三者搞清楚了,写配方文件就跟搭积木一样简单。
- 变量赋值记住优先级:= 最高,?= 中等,??= 最低
- Python函数用 ${@func(d)} 调用,别忘了@符号
- inherit引入类,include/require引入文件
- 文件缺失时,include静默跳过,require报错
好了,这一章的内容就这些。变量赋值、Python函数、继承机制,这三块是Yocto配方文件的核心语法。你写配方时遇到的大部分问题,都能在这三块里找到答案。