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)。

OpenEmbedded 核心概念结构 元数据层(Metadata) Layer(层) → Recipe(配方) → bbclass(类) → inc(包含文件) 配置层(Configuration) Machine(机器) + Distro(发行版) + local.conf(本地配置) 产物层(Output) Image(镜像) + Packagegroup(包组) + Package(单个包) bitbake 解析顺序:Layer优先级 → Machine配置 → Distro配置 → Recipe构建 最终输出:可启动的嵌入式Linux系统镜像

4.1 Layer——组织的艺术

Layer,中文叫“层”。你可以把它理解成一个功能模块的文件夹。每个Layer里放着一组相关的Recipe、配置文件和补丁。

Yocto官方推荐的分层结构是这样的:

  • meta —— OE核心层,包含基础Recipe和bbclass
  • meta-poky —— Poky参考发行版配置
  • meta-yocto-bsp —— 官方支持的板级支持包
  • meta- —— 芯片厂商提供的BSP层
  • meta- —— 你自己的项目层

我的习惯:每个项目至少建两个自定义层——一个叫 meta--bsp 放硬件相关配置,一个叫 meta--app 放应用软件。这样后期维护时,换硬件平台只需要换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/.conf 文件里。

一个典型的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/.conf 里。它主要控制:

  • 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在构建时到底干了什么?

  1. 读取 bblayers.conf,确定有哪些Layer以及它们的优先级
  2. 读取 local.conf,确定MACHINE和DISTRO
  3. 加载Machine配置,确定硬件参数
  4. 加载Distro配置,确定软件生态
  5. 解析Image Recipe,确定要安装哪些包
  6. 递归解析每个包的Recipe,开始下载、编译、安装
  7. 最后把所有包打包成指定格式的镜像文件

这个流程,说白了就是一层层往下解析,一层层往上构建。Layer提供元数据,配置决定行为,Recipe执行具体工作,Image产出最终结果。

我曾经遇到的一个问题:两个Layer里都有同一个Recipe的bbappend文件,结果因为Layer优先级没搞清楚,其中一个补丁没打上,导致驱动编译失败。从那以后,我每次建新Layer都会在 layer.conf 里显式设置 BBFILE_PRIORITY,避免这种隐式依赖。