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编写的六个步骤串起来了。你写Recipe时,就按这个顺序来,基本不会漏东西。
最后说一句:Recipe编写没有太多玄学,多写几个就熟练了。我刚开始时,每写一个Recipe都要对照着别人的改,后来慢慢就形成了自己的模板。你也一样,先模仿,再创造,很快就能上手。