4. 理解OpenEmbedded核心:Layer、Recipe、Machine与Distro
Yocto项目说到底,就是一套用来自定义嵌入式Linux系统的工具链。而OpenEmbedded(简称OE)才是它的灵魂。我刚开始接触Yocto时,最困惑的就是那一堆目录和文件——bb文件、bbappend文件、conf文件……到底谁管谁?
这一章,我们就来拆解OE的核心概念。说白了,你只要搞懂Layer、Recipe、Machine、Distro、Image和Packagegroup这六个东西,Yocto的大门就算被你踹开了。
核心一句话:Yocto = 元数据(Layer/Recipe)+ 配置(Machine/Distro)+ 目标产物(Image/Packagegroup)。
4.1 Layer——组织的艺术
Layer,中文叫“层”。你可以把它理解成一个功能模块的文件夹。每个Layer里放着一组相关的Recipe、配置文件和补丁。
Yocto官方推荐的分层结构是这样的:
- meta —— OE核心层,包含基础Recipe和bbclass
- meta-poky —— Poky参考发行版配置
- meta-yocto-bsp —— 官方支持的板级支持包
- meta-
—— 芯片厂商提供的BSP层 - meta-
—— 你自己的项目层
我的习惯:每个项目至少建两个自定义层——一个叫 meta- 放硬件相关配置,一个叫 meta- 放应用软件。这样后期维护时,换硬件平台只需要换BSP层,应用层基本不动。
Layer的优先级通过 BBLAYERS 变量控制,越靠后的层优先级越高。嗯,这个顺序很容易搞反,我刚开始就踩过这个坑。
# bblayers.conf 示例
BBLAYERS ?= " \
/path/to/meta \
/path/to/meta-poky \
/path/to/meta-yocto-bsp \
/path/to/meta-raspberrypi \
/path/to/meta-myproject \
"
4.2 Recipe——构建的配方
Recipe就是 .bb 文件。它告诉bitbake:从哪里下载源码、怎么打补丁、怎么配置编译、安装到哪个目录。
一个最简单的Recipe长这样:
# hello_1.0.bb
SUMMARY = "A simple hello world program"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
SRC_URI = "file://hello.c"
S = "${WORKDIR}"
do_compile() {
${CC} ${CFLAGS} hello.c -o hello ${LDFLAGS}
}
do_install() {
install -d ${D}${bindir}
install -m 0755 hello ${D}${bindir}
}
这里有几个关键变量:
| 变量 | 含义 | 我的备注 |
|---|---|---|
SUMMARY |
简短描述 | 别写太长,bitbake日志里会显示 |
LICENSE |
许可证类型 | 不写的话,构建会报错 |
SRC_URI |
源码来源 | 支持http、git、file://等协议 |
S |
源码解压后的目录 | 默认是 ${WORKDIR}/${BPN}-${PV} |
do_compile() |
编译任务 | 可以重写,也可以用autotools/cmake继承 |
do_install() |
安装任务 | 把编译产物放到 ${D} 目录下 |
我曾经踩过的坑:写Recipe时忘了设置 LIC_FILES_CHKSUM,结果bitbake报错说找不到许可证文件。后来养成习惯,每次新建Recipe第一件事就是写LICENSE和校验值。
4.3 Machine——告诉系统你用的是啥硬件
Machine配置,说白了就是告诉Yocto:你的目标硬件长什么样。它定义在 conf/machine/ 文件里。
一个典型的Machine配置包含:
- TUNE_FEATURES —— CPU架构特性(armv7a、cortex-a53等)
- MACHINE_FEATURES —— 硬件特性(wifi、bluetooth、screen等)
- PREFERRED_PROVIDER —— 指定使用哪个版本的u-boot、kernel
- SERIAL_CONSOLES —— 串口控制台配置
- IMAGE_FSTYPES —— 生成镜像格式(ext4、sdcard、wic等)
# conf/machine/my-gateway.conf
TUNE_FEATURES = "armv7a cortex-a7 neon"
MACHINE_FEATURES = "wifi bluetooth usbhost"
PREFERRED_PROVIDER_virtual/kernel = "linux-mainline"
SERIAL_CONSOLES = "115200;ttyAMA0"
IMAGE_FSTYPES = "wic.bz2 ext4"
我个人习惯把Machine配置写得尽量精简,只放硬件相关的变量。软件层面的东西,交给Distro去管。
4.4 Distro——定义系统的“性格”
Distro配置决定了你的系统是什么风格。是追求最小体积的嵌入式风格?还是带包管理器的桌面风格?
Distro配置放在 conf/distro/ 里。它主要控制:
- DISTRO_FEATURES —— 系统特性(systemd vs sysvinit、x11 vs wayland等)
- PACKAGE_CLASSES —— 包格式(rpm、deb、ipk)
- EXTRA_IMAGE_FEATURES —— 镜像额外特性(debug-tweaks、ssh-server等)
- TCLIBC —— C库选择(glibc、musl、uclibc)
# conf/distro/my-distro.conf
DISTRO = "my-distro"
DISTRO_NAME = "My Embedded Gateway Distro"
DISTRO_VERSION = "1.0"
DISTRO_FEATURES = "systemd wifi bluetooth usbhost"
PACKAGE_CLASSES = "package_ipk"
TCLIBC = "glibc"
关键区别:Machine管硬件,Distro管软件生态。同一个Machine可以跑不同的Distro,同一个Distro也可以适配不同的Machine。这两者解耦,是Yocto灵活性的核心。
4.5 Image——最终产物的定义
Image就是你要生成的系统镜像。它也是一个Recipe,只不过它的任务不是编译一个软件,而是把一堆包组装成一个可启动的系统。
Yocto自带了一些标准Image:
| Image名称 | 包含内容 | 适用场景 |
|---|---|---|
core-image-minimal |
仅基础系统 | 最小化验证 |
core-image-base |
基础系统+常用工具 | 通用嵌入式 |
core-image-full-cmdline |
完整命令行工具 | 开发调试 |
core-image-sato |
带图形界面 | 桌面/平板 |
自定义一个Image也很简单:
# images/my-gateway-image.bb
SUMMARY = "My Gateway System Image"
LICENSE = "MIT"
inherit core-image
IMAGE_INSTALL = " \
packagegroup-core-boot \
packagegroup-base \
my-gateway-app \
network-manager \
openssh \
"
4.6 Packagegroup——包的分组管理
Packagegroup就是一组包的集合。它让你不用在Image里一个一个列包名,而是按功能分组引用。
举个例子:
# recipes-core/packagegroups/packagegroup-gateway-networking.bb
SUMMARY = "Gateway networking packages"
LICENSE = "MIT"
inherit packagegroup
RDEPENDS:${PN} = " \
network-manager \
iptables \
dnsmasq \
bridge-utils \
"
然后在Image里直接引用:
IMAGE_INSTALL = " \
packagegroup-core-boot \
packagegroup-gateway-networking \
"
我的经验:把包按功能拆成多个Packagegroup——比如 packagegroup-gateway-core、packagegroup-gateway-security、packagegroup-gateway-cloud。这样不同产品变体只需要组合不同的Packagegroup,Image Recipe几乎不用改。
4.7 它们怎么协同工作?
你想想看,bitbake在构建时到底干了什么?
- 读取
bblayers.conf,确定有哪些Layer以及它们的优先级 - 读取
local.conf,确定MACHINE和DISTRO - 加载Machine配置,确定硬件参数
- 加载Distro配置,确定软件生态
- 解析Image Recipe,确定要安装哪些包
- 递归解析每个包的Recipe,开始下载、编译、安装
- 最后把所有包打包成指定格式的镜像文件
这个流程,说白了就是一层层往下解析,一层层往上构建。Layer提供元数据,配置决定行为,Recipe执行具体工作,Image产出最终结果。
我曾经遇到的一个问题:两个Layer里都有同一个Recipe的bbappend文件,结果因为Layer优先级没搞清楚,其中一个补丁没打上,导致驱动编译失败。从那以后,我每次建新Layer都会在 layer.conf 里显式设置 BBFILE_PRIORITY,避免这种隐式依赖。