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函数的入口。我刚开始总忘记写@符号,结果变量一直不展开,排查了半天才发现。

小技巧:在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 () {} - 解析时自动执行 - 不能手动调用 具名函数 def func(d): - 可被其他任务调用 - 用 ${@func(d)} 调用 常用API: d.getVar() / d.setVar() bb.note() / bb.warn() 继承与包含 inherit 类继承 - 引入 .bbclass 文件 - 自动添加任务/变量 include 文件包含 - 文件缺失静默跳过 require 强制包含 - 文件缺失直接报错 推荐:优先用 require 避免 inherit 过多导致冲突 三者配合使用,构建灵活可复用的配方文件

你看这张图,左边是变量赋值,中间是Python函数,右边是继承机制。这三块是互相配合的。比如你可以在Python函数里用d.getVar()读取变量,也可以用d.setVar()修改变量。而inherit进来的类文件里,也可能定义了变量和Python函数。

我个人觉得,理解这三块的核心在于:变量是数据,Python函数是逻辑,inherit/include是组织方式。把这三者搞清楚了,写配方文件就跟搭积木一样简单。

核心要点:
  • 变量赋值记住优先级:= 最高,?= 中等,??= 最低
  • Python函数用 ${@func(d)} 调用,别忘了@符号
  • inherit引入类,include/require引入文件
  • 文件缺失时,include静默跳过,require报错

好了,这一章的内容就这些。变量赋值、Python函数、继承机制,这三块是Yocto配方文件的核心语法。你写配方时遇到的大部分问题,都能在这三块里找到答案。