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 配方结构全景图

下面这张图展示了配方从源码到产物的完整流程,以及继承机制在其中扮演的角色:

Recipe 结构全景图 Recipe 结构全景图 my-app_1.0.bb 描述信息 SUMMARY / LICENSE 依赖关系 DEPENDS / RDEPENDS 源码获取 SRC_URI / SRCREV 补丁管理 *.patch 文件 任务 do_* inherit 机制:引入 .bbclass 文件中的通用逻辑 autotools.bbclass cmake.bbclass systemd.bbclass 构建产物 .ipk / .rpm / .deb 包 关键说明: • 配方文件通过 SRC_URI 获取源码,应用补丁后执行 do_configure → do_compile → do_install • inherit 机制在解析阶段就注入类中的变量和任务定义,影响整个构建流程 • 五要素中任何一个出错,都会导致构建失败或产物异常

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 的配置选项就要写几十行代码。

我的习惯: 每个新配方我都会先写一个最小版本——只包含描述和源码获取,确认能下载后再逐步添加补丁和自定义任务。这样排查问题时会非常高效。