4. Recipe编写实战:从基本结构到自定义示例

Recipe,说白了就是告诉BitBake怎么去下载、配置、编译和安装一个软件包。很多初学者觉得它很神秘,其实拆开来看,就那么几块东西。我刚开始接触Yocto时,也被那堆.bb文件搞得头晕,后来发现只要掌握了骨架,剩下的就是往里填肉。

4.1 Recipe基本结构

一个标准的Recipe文件,通常长这样:

SUMMARY = "一个简单的示例程序"
DESCRIPTION = "这是我用来演示Recipe结构的测试程序"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://LICENSE;md5=xxxx"

SRC_URI = "file://hello.c \
           file://Makefile \
           file://0001-fix-build.patch"

S = "${WORKDIR}"
inherit autotools

do_compile() {
    make
}

do_install() {
    install -d ${D}${bindir}
    install -m 0755 hello ${D}${bindir}
}

这里有几个关键变量,我一个个说:

  • SUMMARY / DESCRIPTION:包的简短描述和详细说明,别小看,bitbake-layers show-recipes就靠这个显示信息。
  • LICENSE:许可证类型,Yocto对许可证检查很严格,写错了编译会报错。
  • LIC_FILES_CHKSUM:许可证文件的校验和,我第一次写时忘了加这个,结果bitbake直接罢工。
  • SRC_URI:源代码来源,这个后面重点讲。
  • S:源码解压后的目录,默认是${WORKDIR}/${PN}-${PV},但很多情况需要手动指定。
  • inherit:继承的类,相当于复用别人的轮子。

核心要点:Recipe的本质就是定义变量和任务函数。变量告诉BitBake"用什么",任务函数告诉它"怎么做"。

4.2 SRC_URI详解

SRC_URI是Recipe里最灵活的变量之一。它支持多种协议,我整理了一张表:

协议 写法示例 适用场景
本地文件 file://hello.c 放在Recipe同目录或FILESEXTRAPATHS路径下
HTTP/HTTPS https://example.com/pkg.tar.gz 从网络下载源码包
Git仓库 git://github.com/user/repo.git;branch=master 从Git拉取源码,支持分支、标签、commit
SVN svn://example.com/svn/trunk Subversion版本控制
本地目录 file:///home/user/mylib 绝对路径指向本地源码目录

Git协议用得最多,我习惯加几个常用参数:

SRC_URI = "git://github.com/myproject/myapp.git;protocol=https;branch=main;rev=v1.0"

这里protocol=https是绕过某些公司防火墙的Git端口限制,rev可以指定tag或commit hash。我曾经踩过一个坑:没指定rev,结果每次构建都拉最新的代码,版本完全不可控。

小技巧:如果源码包很大,建议用;name=xxx给每个SRC_URI条目命名,方便在do_fetch里单独处理。

4.3 补丁管理

嵌入式开发中,给上游代码打补丁是家常便饭。Yocto的补丁管理其实很优雅——你只需要把补丁文件放在Recipe同目录,然后在SRC_URI里引用就行。

补丁文件的命名,我推荐用这种格式:

0001-fix-memory-leak-in-parser.patch
0002-add-missing-header-file.patch
0003-workaround-for-kernel-5.10.patch

数字前缀控制应用顺序,后面跟简短描述。BitBake会按文件名排序依次打补丁。

补丁的生成方式,我常用两种:

  • quilt工具:进入源码目录,用quilt new创建补丁,quilt add添加文件,修改后quilt refresh导出。
  • git format-patch:如果源码本身就是Git仓库,直接用git format-patch生成标准补丁。

注意:补丁文件里不要包含绝对路径。我曾经犯过这个错,补丁打上去后路径对不上,排查了半天才发现是补丁里的路径写死了。

另外,如果补丁需要条件应用,可以用变量控制:

SRC_URI_append = " ${@bb.utils.contains('MACHINE_FEATURES', 'wifi', 'file://enable-wifi.patch', '', d)}"

这段代码的意思是:如果MACHINE_FEATURES里包含wifi,就追加wifi相关的补丁。嗯,条件编译在Yocto里就是这么玩的。

4.4 继承与类

Yocto提供了大量现成的类(classes),放在meta/classes/目录下。继承一个类,相当于把该类定义的所有变量和任务函数导入到当前Recipe中。

常用的类有这些:

类名 作用 我常用的场景
autotools 处理autoconf/automake项目 大多数开源库
cmake 处理CMake项目 现代C++项目
meson 处理Meson构建系统 GNOME/GTK相关
update-rc.d 管理系统服务开机启动 自研守护进程
systemd 集成systemd服务单元 使用systemd的系统

举个例子,如果你继承autotools,BitBake会自动帮你做这些事情:

  • do_configure:运行./configure
  • do_compile:运行make
  • do_install:运行make install

你想想看,如果每个Recipe都要自己写这些任务函数,那得多累。我刚开始做Yocto时,有个项目用了20多个库,每个都手写do_configure,后来发现inherit autotools一行就搞定了,当时真想抽自己。

经验之谈:如果继承的类不能满足需求,可以重写(override)它的任务函数。比如autotools的do_configure默认参数不够,你可以这样:

do_configure() {
    ./configure --prefix=/usr --enable-feature-x
}

这样就会覆盖类里的默认实现。

4.5 自定义Recipe示例

光说不练假把式。我们来写一个完整的自定义Recipe,目标是把一个简单的C程序编译并安装到目标系统。

先看源码文件结构:

recipes-example/hello/
├── files
│   ├── hello.c
│   ├── Makefile
│   └── 0001-fix-typo.patch
└── hello_1.0.bb

hello.c内容:

#include <stdio.h>

int main() {
    printf("Hello from Yocto!\n");
    return 0;
}

Makefile内容:

CC = gcc
CFLAGS = -Wall -O2

hello: hello.c
    $(CC) $(CFLAGS) -o $@ $^

install:
    install -d $(DESTDIR)/usr/bin
    install -m 0755 hello $(DESTDIR)/usr/bin

clean:
    rm -f hello

补丁文件0001-fix-typo.patch:

--- a/hello.c
+++ b/hello.c
@@ -1,6 +1,6 @@
 #include <stdio.h>

 int main() {
-    printf("Hello from Yocto!\n");
+    printf("Hello from Yocto! (patched)\n");
     return 0;
 }

最后是Recipe文件hello_1.0.bb:

SUMMARY = "Hello World示例程序"
DESCRIPTION = "一个简单的Yocto Recipe教学示例"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

SRC_URI = "file://hello.c \
           file://Makefile \
           file://0001-fix-typo.patch"

S = "${WORKDIR}"

do_compile() {
    oe_runmake
}

do_install() {
    oe_runmake install DESTDIR=${D}
}

这里有几个细节:

  • LIC_FILES_CHKSUM:我直接引用了Yocto自带的MIT许可证文件,省得自己再复制一份。
  • oe_runmake:这是BitBake提供的封装函数,会自动传递交叉编译的环境变量。
  • DESTDIR=${D}:安装时一定要指定DESTDIR,否则会装到宿主机上,而不是目标系统的根文件系统里。

避坑指南:我曾经在do_install里忘了加DESTDIR,结果make install直接把文件装到了/usr/bin下,差点把宿主机的系统搞乱。从那以后,我每次写Recipe都会检查安装路径。

4.6 Recipe编写流程总结

为了让你对整个流程有个直观认识,我画了一张图:

Recipe编写与构建流程 1. 创建Recipe文件 2. 定义SRC_URI 3. 添加补丁文件 4. 继承合适的类 5. 编写任务函数 6. bitbake验证 关键注意事项 • 文件名格式:包名_版本.bb • SRC_URI支持多种协议 • 补丁按文件名排序应用 • 继承类可复用构建逻辑 • 任务函数可重写或追加 • 安装必须指定DESTDIR • 使用oe_runmake代替make • LIC_FILES_CHKSUM不能少 • 补丁路径不能写绝对路径 • 条件补丁用append控制 • 调试用bitbake -c devshell • 清理用bitbake -c clean • 查看依赖用bitbake -g

这张图把Recipe编写的六个步骤串起来了。你写Recipe时,就按这个顺序来,基本不会漏东西。

最后说一句:Recipe编写没有太多玄学,多写几个就熟练了。我刚开始时,每写一个Recipe都要对照着别人的改,后来慢慢就形成了自己的模板。你也一样,先模仿,再创造,很快就能上手。