4. Recipe编写入门:从零开始构建你的第一个配方

Recipe,说白了就是Yocto的“菜谱”。你想想看,做菜需要食材清单、步骤说明,Recipe也是一样——它告诉BitBake去哪里下载源码、怎么解压、怎么打补丁、怎么编译、最后装到哪。今天我就带你手把手拆解一个完整的Recipe。

4.1 Recipe文件结构:骨架先搭好

一个典型的Recipe文件长什么样?我直接给你看个例子:

SUMMARY = "一个简单的Hello World程序"
DESCRIPTION = "这个程序会在终端打印Hello World,用于演示Recipe编写"
HOMEPAGE = "https://example.com"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://LICENSE;md5=xxxxx"

SRC_URI = "git://github.com/example/hello.git;branch=main"

S = "${WORKDIR}/git"

inherit autotools

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

嗯,这里要注意几个关键变量:

  • SUMMARY/DESCRIPTION:简短描述和详细说明,别偷懒,写清楚对后面维护有好处
  • HOMEPAGE:项目主页,方便别人查资料
  • SECTION:分类标签,比如console/utils、libs、devel等
  • LICENSE:许可证声明,这个后面细说
  • SRC_URI:源码来源,Recipe的灵魂所在
  • S:源码解压后的目录,默认是${WORKDIR}/${BPN}-${PV}
我的习惯:每次新建Recipe,我都会先写SUMMARY和LICENSE,再写SRC_URI。顺序不重要,但养成固定习惯能减少遗漏。

4.2 LICENSE与LIC_FILES_CHKSUM:合规的底线

许可证这事,说实话很多人不重视。我在项目中遇到过客户审计,发现一个第三方库的LICENSE没写对,结果整个BSP都要重新发布。那叫一个折腾。

LICENSE变量声明许可证类型,比如:

LICENSE = "GPL-2.0-only"
LICENSE = "MIT & BSD-3-Clause"
LICENSE = "LGPL-2.1+"

多个许可证用空格分隔,&表示“且”,|表示“或”。

LIC_FILES_CHKSUM是校验文件,格式是:

LIC_FILES_CHKSUM = "file://COPYING;md5=xxxxx"

为什么要加这个?说白了就是防止有人偷偷改许可证。BitBake会计算文件的MD5值,跟你写的比对,不一致就报错。

我曾经踩过的坑:有一次我复制了别人的Recipe,忘了改LIC_FILES_CHKSUM的MD5值。结果编译时一直报校验失败,查了半天才发现是MD5对不上。后来我养成了习惯——每次写完Recipe,先跑一遍bitbake -c fetch recipe-name,看校验值对不对。

获取MD5值的方法:

# 方法1:手动计算
md5sum path/to/LICENSE

# 方法2:让BitBake帮你算(推荐)
bitbake -c fetch recipe-name
# 然后看报错信息里的expected值

4.3 SRC_URI详解:源码从哪里来

SRC_URI是Recipe里最灵活的变量。它支持多种协议:

协议 示例 说明
HTTP/HTTPS https://example.com/foo.tar.gz 最常用,支持wget下载
Git git://github.com/xxx/yyy.git;branch=master 克隆仓库,支持分支、tag、commit
本地文件 file://mypatch.patch 放在Files目录下的文件
SVN svn://example.com/repo Subversion仓库

Git协议最常用,参数也最多:

SRC_URI = "git://github.com/example/libfoo.git;protocol=https;branch=main;tag=v1.0"

参数说明:

  • protocol:协议类型,git、https、ssh等
  • branch:分支名
  • tag:标签名,指定后就不需要branch了
  • rev:commit ID,最精确的方式
  • destsuffix:解压后的目录名,默认是git
我的建议:生产环境里,尽量用commit ID而不是branch。为什么?因为branch会变,今天编译通过,明天可能就挂了。用commit ID保证可重现。

4.4 do_fetch / do_unpack / do_patch:源码准备三步走

这三个任务是BitBake自动执行的,但理解它们能帮你排查问题。

do_fetch:下载源码

  • 根据SRC_URI下载源码包
  • 下载到${DL_DIR}目录
  • 支持断点续传,我经常用bitbake -c fetchall recipe-name提前下载所有依赖

do_unpack:解压源码

  • 解压到${WORKDIR}目录
  • tar.gz自动解压,git仓库自动克隆
  • 解压后的目录名由S变量指定

do_patch:打补丁

  • 应用SRC_URI中列出的所有补丁文件
  • 补丁文件放在Recipe同目录的Files文件夹下
  • 按文件名顺序应用,建议用数字前缀控制顺序
# 补丁文件示例
SRC_URI += "file://0001-fix-build-error.patch"
SRC_URI += "file://0002-add-feature.patch"
小技巧:如果你想知道当前Recipe执行到哪一步了,可以用bitbake -c listtasks recipe-name查看所有任务。我调试时经常用这个命令。

4.5 do_configure / do_compile / do_install:核心构建三件套

这三个任务才是真正干活的地方。默认情况下,BitBake会调用autotools或cmake的标准流程,但很多时候你需要自定义。

do_configure:配置阶段

  • 默认执行./configure(autotools)或cmake(CMake)
  • 可以添加额外参数:EXTRA_OECONF或EXTRA_OECMAKE
  • 也可以完全重写:
do_configure() {
    # 自定义配置
    ./configure --prefix=/usr --enable-feature
}

do_compile:编译阶段

  • 默认执行make
  • 可以指定并行度:PARALLEL_MAKE = "-j4"
  • 也可以完全重写:
do_compile() {
    oe_runmake
}

do_install:安装阶段

  • 把编译产物安装到${D}目录(临时根文件系统)
  • ${D}默认是${WORKDIR}/image
  • 安装路径要对应目标系统的目录结构
do_install() {
    install -d ${D}${bindir}
    install -m 0755 myprogram ${D}${bindir}
    install -d ${D}${sysconfdir}
    install -m 0644 myconfig.conf ${D}${sysconfdir}
}
我曾经犯过的错:有次写do_install,忘了创建目录就直接install文件,结果编译报错说找不到路径。后来我养成了习惯——先install -d创建目录,再install文件。顺序别搞反。

4.6 实战:写一个完整的Recipe

光说不练假把式。我们来写一个完整的Recipe,编译一个简单的C程序:

# hello_1.0.bb
SUMMARY = "Hello World示例程序"
DESCRIPTION = "一个简单的Hello World程序,用于演示Recipe编写流程"
HOMEPAGE = "https://example.com"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://LICENSE;md5=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

SRC_URI = "file://hello.c \
           file://LICENSE"

S = "${WORKDIR}"

do_compile() {
    ${CC} ${CFLAGS} ${LDFLAGS} hello.c -o hello
}

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

对应的Files目录结构:

recipes-example/hello/
├── hello_1.0.bb
└── files/
    ├── hello.c
    └── LICENSE

hello.c内容:

#include <stdio.h>

int main() {
    printf("Hello from Yocto!\n");
    return 0;
}
关键点:注意我用了${CC}、${CFLAGS}、${LDFLAGS}这些变量。这是Yocto提供的交叉编译工具链变量,千万别写死gcc,否则在交叉编译环境下会出问题。

编译这个Recipe:

bitbake hello

如果一切顺利,你会在目标系统的/usr/bin目录下找到hello程序。运行它,就会打印出"Hello from Yocto!"。

嗯,写Recipe其实没那么难。核心就是搞清楚:源码从哪来、怎么编译、装到哪去。这三个问题搞定了,Recipe就写好了。剩下的就是多写多练,遇到问题多查日志。我在项目里写了上百个Recipe,现在闭着眼睛都能写出来。你也行的。