4. 配方(Recipe)基础结构:从命名到继承
配方是 Yocto 项目的核心单元。说白了,每个配方就是一份「烹饪说明书」——告诉 BitBake 从哪里下载源码、怎么打补丁、编译成什么、装到哪个目录。我刚开始接触 Yocto 时,觉得配方就是个 .bb 文件,后来踩了不少坑才明白,里面的门道比想象中多得多。
4.1 Recipe 文件命名规范
配方文件的命名不是随便起的。它有一套严格的约定,直接决定了 BitBake 如何解析版本、优先级和覆盖关系。
| 组成部分 | 示例 | 说明 |
|---|---|---|
| 包名 | openssh | 小写字母、数字、连字符,不能有下划线 |
| 版本号 | 9.6p1 | 数字和字母组合,用小数点分隔 |
| 扩展名 | .bb | 普通配方用 .bb,附加配方用 .bbappend |
完整的文件名长这样:
openssh_9.6p1.bb
这里有个细节:版本号中的字母会被 BitBake 特殊处理。比如 9.6p1 中的 p 会被识别为「补丁版本」,排序时 9.6p1 大于 9.6 但小于 9.7。嗯,这个规则我当年查了好久才搞明白。
my-lib_1.0-beta.bb,结果 BitBake 把 -beta 解析成版本号的一部分,导致版本比较完全乱掉。建议只用纯数字和标准字母后缀(如 rc1、p1)。
4.2 Recipe 文件组成
一个完整的配方通常包含五个核心部分。我习惯把它们称为「配方五要素」。
4.2.1 描述信息
每个配方开头都应该有描述信息。这不是摆设——当你用 bitbake -s 查看包列表时,这些描述会直接显示出来。
SUMMARY = "OpenSSH - secure shell client and server"
DESCRIPTION = "OpenSSH is a free version of the SSH connectivity tools..."
HOMEPAGE = "https://www.openssh.com/"
LICENSE = "BSD-2-Clause"
LIC_FILES_CHKSUM = "file://LICENCE;md5=..."
LIC_FILES_CHKSUM 这个变量特别重要。它记录了许可证文件的校验和,如果源码中的许可证文件被修改,BitBake 会直接报错。我在项目中遇到过好几次,因为更新了源码版本但忘了更新校验和,导致构建失败。
4.2.2 依赖关系
依赖是配方的骨架。Yocto 把依赖分为编译时依赖和运行时依赖:
# 编译时依赖
DEPENDS = "zlib openssl"
# 运行时依赖
RDEPENDS:${PN} = "glibc libcrypto"
这里有个容易混淆的点:DEPENDS 是全局的,而 RDEPENDS:${PN} 中的 ${PN} 会自动展开为包名。我个人习惯把运行时依赖写成 RDEPENDS:${PN} 而不是 RDEPENDS,这样更清晰。
DEPENDS 是否完整。我常用的方法是 bitbake -g <recipe> 生成依赖图,然后用 depends.dot 文件可视化查看。
4.2.3 源码获取
源码从哪里来?Yocto 支持多种协议:
SRC_URI = "https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-${PV}.tar.gz"
SRC_URI[sha256sum] = "..."
# 也可以从 Git 仓库获取
SRC_URI = "git://github.com/openssh/openssh-portable.git;branch=master"
SRCREV = "a1b2c3d4e5f6..."
${PV} 是 BitBake 自动从文件名中提取的版本号变量。你想想看,如果手动写死版本号,每次升级都要改好几个地方,多麻烦。
4.2.4 补丁管理
补丁是配方裁剪中最常用的手段。Yocto 支持两种补丁方式:
# 方式一:直接列出补丁文件
SRC_URI += "file://fix-cross-compile.patch"
# 方式二:使用 quilt 管理(推荐)
SRC_URI += "file://0001-fix-cross-compile.patch;apply=yes"
我个人强烈推荐使用 quilt 格式的补丁。为什么?因为补丁文件名中的序号(如 0001-)决定了应用顺序,而且 quilt 可以自动处理补丁的堆叠和回退。我曾经接手过一个项目,里面全是无序号、无描述的补丁文件,排查问题时简直想哭。
- 每个补丁只做一件事
- 补丁描述要写清楚「为什么」而不是「做了什么」
- 优先使用
Upstream-Status标记补丁状态
4.2.5 任务定义
配方中的任务(task)是 BitBake 执行的最小单元。最常用的几个任务:
do_configure() {
# 配置步骤
./configure --host=${HOST_SYS} --prefix=/usr
}
do_compile() {
# 编译步骤
oe_runmake
}
do_install() {
# 安装步骤
oe_runmake install DESTDIR=${D}
}
注意 ${D} 是临时安装目录,不是最终的系统根目录。这个区别很重要——我见过有人直接把文件装到 /usr/bin 下,结果构建出来的包完全不可用。
4.3 继承(inherit)与类(class)文件
继承是 Yocto 配方复用的核心机制。说白了,就是把通用的逻辑抽到 .bbclass 文件里,然后配方通过 inherit 关键字引入。
4.3.1 常用内置类
| 类名 | 作用 | 使用场景 |
|---|---|---|
| autotools | 自动处理 configure/make/make install | 使用 autotools 构建的项目 |
| cmake | 封装 CMake 构建流程 | 使用 CMake 的项目 |
| meson | 封装 Meson 构建流程 | 使用 Meson 的项目 |
| update-rc.d | 管理系统服务启停脚本 | 需要安装 init 脚本的包 |
| systemd | 处理 systemd unit 文件 | 使用 systemd 的系统 |
使用方式很简单:
inherit autotools systemd
一行代码,BitBake 就会自动注入 configure、compile、install 等任务的默认实现。我刚开始用 Yocto 时,总觉得继承太多类会让配方变得「黑盒」,后来才明白——这正是 Yocto 的设计哲学:把通用逻辑交给类,配方只关注差异部分。
4.3.2 自定义类文件
当内置类不够用时,可以自己写 .bbclass 文件。比如我经常写一个 my-debug.bbclass:
# my-debug.bbclass
DEBUG_FLAGS ?= "-g -O0"
CFLAGS += "${DEBUG_FLAGS}"
CXXFLAGS += "${DEBUG_FLAGS}"
do_compile:append() {
bbnote "Compiling with debug flags: ${DEBUG_FLAGS}"
}
然后在配方中继承:
inherit my-debug
自定义类文件要放在 meta-<layer>/classes/ 目录下。我建议每个层都建一个 classes/ 目录,把通用的逻辑抽出来,这样配方会清爽很多。
inherit 的顺序会影响变量的最终值。后继承的类会覆盖先继承的类中定义的变量。我曾经因为继承顺序搞反,导致 CFLAGS 被错误覆盖,排查了整整一天。
4.4 配方结构全景图
下面这张图展示了配方从源码到产物的完整流程,以及继承机制在其中扮演的角色:
4.5 实战:一个完整的配方示例
把上面讲的内容串起来,写一个完整的配方:
# hello-world_1.0.bb
SUMMARY = "Simple hello world application"
DESCRIPTION = "A minimal hello world program for Yocto demo"
HOMEPAGE = "https://example.com/hello-world"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://LICENSE;md5=..."
SRC_URI = "git://github.com/example/hello-world.git;branch=main \
file://0001-fix-typo.patch \
file://hello-world.service"
SRCREV = "v1.0"
inherit autotools systemd
DEPENDS = "glibc"
RDEPENDS:${PN} = "glibc"
SYSTEMD_SERVICE:${PN} = "hello-world.service"
do_install:append() {
install -d ${D}${systemd_system_unitdir}
install -m 0644 ${WORKDIR}/hello-world.service ${D}${systemd_system_unitdir}/
}
这个配方虽然简单,但包含了我们今天讲的所有要素:描述、依赖、源码、补丁、任务,以及继承。你想想看,如果没有继承机制,光处理 autotools 的配置选项就要写几十行代码。