3 Yocto Project 概念

本章解释操作指南和参考资料背后的 Yocto Project 概念,包括组件、 OpenEmbedded Build System 流程、交叉开发工具链、共享状态缓存等。

备注

本章介绍许多变量及其含义。要在 Build Directory 上下文中查看 任意变量值,可用 bitbake-getvar,见 “查看变量值”。

3 Yocto Project 组件

BitBake 任务执行器和各类配置文件构成 OpenEmbedded-Core (OE-Core)。本节概述这些组件的用途及交互。

BitBake 解析并执行数据文件,数据包括:

  • 配方: 描述具体软件。

  • 类数据: 抽象公共构建信息,例如如何构建 Linux 内核。

  • 配置数据: 定义机器设置、策略等,将各部分连接起来。

BitBake 能组合多个数据源,每个数据源称为层。层介绍见 “理解和创建层”。

下面简述核心组件。构建时的交互详见 “OpenEmbedded 构建系统概念”。

3.1.1 BitBake

BitBake 是构建系统核心,负责解析 Metadata、生成任务列表并执行。 这里仅作简介,更多信息见 BitBake 用户手册。

查看支持的选项:

$ bitbake -h
$ bitbake --help

最常用形式为 bitbake recipename,recipename 是要构建的配方,即目标。 通常对应文件名第一部分,例如 foo_1.3.0.bb 的 foo。处理 matchbox-desktop_1.2.3.bb 可运行:

$ bitbake matchbox-desktop

可能存在多个 matchbox-desktop 版本,BitBake 按发行版配置选择。不同版本 和提供者的选择规则见 “Preferences”。

BitBake 也会先执行依赖任务。例如交叉编译器和 glibc 尚未构建时,会先构建 它们,再构建 matchbox-desktop。

-k 或 --continue 可让构建在出错后尽可能继续。失败目标及依赖它的 目标不能重建,但其他依赖仍可处理。

3 配方

.bb 文件称为配方,通常描述一个软件,包括原始源码下载位置、需要的补丁、 特殊配置选项、编译方式及输出打包方式。

有时也用 package 指代配方,但本手册将“软件包”专用于 .ipk、.deb、.rpm 等打包输出,避免混淆。

3 类

.bbclass 保存配方间可共享的信息。例如 autotools* 包含使用 GNU Autotools 构建应用的通用设置。 详情见参考手册的 “类”。

3 配置

配置文件 (.conf)定义控制构建过程的变量, 涵盖机器、发行版、编译器调优、通用配置,以及构建目录中 build/conf/local.conf 的用户设置。

3 层

层是包含相关元数据指令的仓库,指导系统构建目标。 Yocto Project 层模型 促进协作、 共享、定制和复用。层按逻辑隔离信息,例如将特定硬件配置放在独立层,使其他 可跨硬件共用的元数据放在另一层中共享。

现有层很多,可以从 Yocto Project 兼容层索引 和 OpenEmbedded 层索引 中选取或借鉴。

层按惯例遵循特定结构,使 BitBake 知道到哪里查找各类元数据。创建流程和 bitbake-layers 等工具见 “理解和创建层”。

3 OpenEmbedded 构建系统概念

本节深入介绍项目所用构建系统内部过程,其核心是任务执行器 BitBake。

下图展示高级流程,后文分别展开输入、输出、处理过程和元数据逻辑模块。

../_images/yp-flow-diagram.svg

流程包括:

  • 用户配置: 用户用来控制构建的元数据。

  • 元数据层: 提供软件、机器和发行版元数据的各层。

  • 源文件: 上游发行包、本地项目和 SCM。

  • 构建系统: BitBake 控制的过程,包括获取、打补丁、编译、分析输出、 创建并测试软件包、生成镜像和交叉开发工具。

  • 软件包源: 保存 RPM、DEB 或 IPK 输出包的目录,供构建镜像或 SDK。 若目标启用运行时包管理,也可通过 Web 等方式复制共享这些目录,扩展或 更新已部署镜像。

  • 镜像: 流程生成的镜像。

  • 应用开发 SDK: 随镜像一起或由 BitBake 单独生成的交叉开发工具。

3 用户配置

用户配置定义目标架构、源码下载位置及其他构建属性。

下图展开 总体流程 中的 User Configuration:

../_images/user-configuration.svg

BitBake 需要基础 .conf 文件完成构建。最少所需文件的示例位于构建目录 conf 子目录。

初始化环境 时, 可以指定 Source Directory。

若构建目录不存在,初始化会创建它。BitBake 的全部构建工作在其中进行。 conf 子目录包含默认 build/conf/local.conf 和 build/conf/bblayers.conf。仅当加载设置脚本时这些文件尚不 存在,才会生成默认版本。

配置文件提供定义环境的基础变量。可配置项示例见 OE-Core 的 local.conf.sample。

以下列表并不穷尽:

备注

local.conf 中的设置也可以放在 build/conf/site.conf 和 build/conf/auto.conf。

bblayers.conf 告诉 BitBake 使用哪些层,默认包含构建系统的最小必需层。 自定义层需要手动加入,见 “启用层”。

初始化脚本不创建 site.conf 和 auto.conf。需要时自行创建 auto.conf, 它通常由自动构建器生成:

  • site.conf: 可为多个构建目录设置公共默认值,例如用 PACKAGE_CLASSES 统一软件包格式。

  • auto.conf: 通常由自动构建器写入,内容类似 local.conf 或 site.conf。

可以编辑所有配置文件,进一步定义具体环境,图中对应 User Configuration Edits。

运行 bitbake target 时,BitBake 汇总配置。读取顺序固定为 site.conf、 auto.conf、local.conf,并应用 BitBake 用户手册 “Syntax and Operators” 的普通赋值规则。 因此顺序会影响同一变量。例如 auto.conf 和 local.conf 分别赋不同值,后读 的 local.conf 最终生效。

3 元数据、机器配置和策略配置

上一节介绍全局用户配置,本节进一步看提供软件、机器和策略元数据的层。

总体流程图 中,用户配置下面有三类输入:

  • 元数据(.bb 和补丁): 软件层包含用户配方、补丁和追加文件。例如 层索引 中的 meta-qt5,支持桌面、嵌入式和移动跨平台框架 Qt 5.0。

  • 机器 BSP 配置: 提供特定硬件和目标架构配置。Poky 的示例是 meta-yocto-bsp,参阅 参考嵌入式发行版(Poky)。

  • 策略配置: 发行版层为该发行版镜像和 SDK 提供高层通用策略。Poky 使用 meta-poky,conf/distro 中的 poky.conf 包含许多策略。

下面展开这三类层:

../_images/layer-input.svg

层结构一般相似:分发时需要许可证文件,例如 COPYING.MIT;推荐提供 README, 尤其是要分发时;还包含配置目录和配方目录。结构见 “创建自己的层”,层及可用资源见 “层” 和 “Yocto Project 层模型”。

上述链接列出许多可用层,源码仓库 的 Yocto Metadata Layers 分组也提供这些内容。

备注

源码仓库中部分层不在 OpenEmbedded 层索引,通常因为已弃用或仍属实验性质。

BitBake 通过用户配置 bblayers.conf 确定要使用的层。

3 发行版层

发行版层提供策略配置,推荐独立隔离。conf/distro/distro.conf 中的设置 会覆盖构建目录 local.conf 中同类设置。

典型内容如下,meta-poky 就是此类层:

  • classes、classes-global、classes-recipe: .bbclass 保存配方共享功能。 配方继承后获得其设置和函数,见 “类”。

  • conf: 保存层配置 conf/layer.conf、发行版配置 conf/distro/distro.conf 及发行版范围的包含文件。

  • recipes-*: 影响整个发行版公共功能的配方和追加文件,例如专用配置、 初始化脚本、定制镜像。目录例子有 recipes-core、recipes-extra,内部 层级可变化,通常含 .bb、.bbappend 和发行版专用配置目录等。

3 BSP 层

BSP 层提供面向具体硬件的机器配置,所有内容都与目标机器有关。通用结构见 Yocto Project 板级支持包(BSP)手册。

备注

要符合 Yocto Project 兼容要求,BSP 层必须满足一些结构要求。

conf 包含机器配置 conf/machine/machine.conf 以及层配置 conf/layer.conf。

其他部分按功能分为 recipes-bsp、recipes-core、recipes-graphics、 recipes-kernel 等,可包含多种外形规格、图形支持系统的元数据。

备注

图中虽有多个 recipes-*,并非每个 BSP 都有全部目录。

3 软件层

软件层提供额外软件包的元数据,不包含应放在各自层中的发行版或机器专用信息。 它包含项目需要的配方、追加文件和补丁。

3 源码

生成镜像或其他目标需要访问源码。总体流程图的 Upstream Project Releases、 Local Projects、SCMs (optional) 表示来源,Source Materials 表示也参与 定位源码的镜像源。

源码如何组织由项目决定。已发布软件通常用归档文件固定发行状态;动态或实验 项目可能放在 Git 等 SCM 仓库,允许选择具体修订。也可结合两种方式。

无论来源如何,每个配方都通过 SRC_URI 指向源码。

DL_DIR 是已下载源码缓存。还可通过 BB_GENERATE_MIRROR_TARBALLS 让系统为 Git 仓库生成归档并保存在这里, 这不是默认行为。

合理使用下载缓存可避免重复联网。建议将 DL_DIR 放在构建目录之外,这样 删除构建目录也不会误删下载内容。

下面进一步展示来源和镜像源:

../_images/source-input.svg

3 上游项目发行包

发行包以 tarball、zip 等归档存在于任意位置,与各配方对应。图中展示 BusyBox、 Qt 和 Dbus 的特定版本,实际上任何可通过配方构建的发行产品都可采用这种形式。

3 本地项目

本地项目是用户提供的自定义软件,位于项目本地某处,例如团队检入开发源码树 的目录。

标准方式是使用 externalsrc,在 local.conf 或追加文件 中设置或覆盖配方,使其从指定本地目录获取源码。

3 源码管理系统(可选)

构建系统也可通过支持 Git、Subversion 等 SCM 的 Fetchers 克隆或检出 仓库。do_fetch 根据 SRC_URI 及前缀选择正确模块。

备注

Git 仓库归档并保存在 DL_DIR 的方法见 BB_GENERATE_MIRROR_TARBALLS。

获取仓库时,BitBake 用 SRCREV 确定具体构建修订。

3 源码镜像源

有前置镜像和普通镜像两类,分别由 PREMIRRORS、MIRRORS 指定。 BitBake 在访问上游前检查前置镜像,适用于 DL_DIR 之外的共享目录,通常是 组织内部的位置。

普通镜像可以是互联网任意站点,在主站不可用时作为替代源码位置。

3 软件包源

生成镜像或 SDK 时,构建系统从构建目录中的软件包源区域取包,在总体流程图 右上角表示。

下面展开该区域:

../_images/package-feeds.svg

软件包源是构建中间阶段。系统提供生成不同包类型的类,通过 PACKAGE_CLASSES 选择。在放入软件包源前,构建系统会验证包生成过程 中的输出质量。

这些质量检查由 insane 类完成。

软件包源位于构建目录,具体暂存位置由变量和包管理器共同决定。图中右侧所列 关键变量为:

do_package_write_* 生成并放置软件包, 例如 IPK 使用 do_package_write_ipk。详情见 do_package_write_deb、 do_package_write_ipk、do_package_write_rpm。 若使用 IPK 且支持 i586 与 qemux86,则分别放到 build/tmp/deploy/ipk/i586 和 build/tmp/deploy/ipk/qemux86。

3 BitBake 工具

构建系统用 BitBake 生成镜像和 SDK。总体流程图的 BitBake 区域包含多个功能 部分,下面逐一展开。

备注

BitBake 有独立文档,参阅 BitBake 用户手册。

3 获取源码

配方构建首先获取并解包源码:

../_images/source-fetching.svg

do_fetch 和 do_unpack 将文件获取并解包到构建目录。

备注

对 SRC_URI 中每个 file:// 本地文件,系统计算校验和并加入 fetch 任务 签名。任一本地文件变化,fetch 及全部依赖它的任务都会重新执行。

默认全部工作在具有固定结构的构建目录进行,详见 build/。

每个配方都有解包源码区域,由 UNPACKDIR 指向,默认名称 sources。 图示层级中的变量为:

  • TMPDIR:构建全部工作的基础目录,默认为 tmp。

  • PACKAGE_ARCH:输出包架构,随目的地而异,可能是目标架构、构建 主机、SDK 或具体机器,详见变量说明。

  • TARGET_OS:目标操作系统,通常为 linux,例如 qemux86-poky-linux。

  • PN:生成包的配方名称。它在其他上下文可能有不同含义,但在输入 文件上下文中指配方名。

  • WORKDIR:构建配方、创建软件包的工作位置。

    • PV:配方版本。

  • UNPACKDIR:该配方解包后的源文件。

  • S:源码最终位置。

    BP 默认为 ${BPN}-${PV},其中:

    • BPN:配方名称,是去掉常见前后缀的 PN。

    • PV:配方版本。

备注

图中分别展示基于 PACKAGE_ARCH 和 MACHINE 的两种层级,底层结构相同, 区别在于构建目标是通用架构、主机、SDK 还是具体机器。

3 打补丁

获取并解包后,BitBake 查找补丁并应用到源码:

../_images/patching.svg

do_patch 结合 SRC_URI 和 FILESPATH 定位适用补丁。

默认识别 *.patch、*.diff 及其压缩形式,也可通过 SRC_URI 参数 改变识别方式,详见该任务文档。

同一配方多个补丁按找到的顺序应用。FILESPATH 定义默认搜索目录,找到后 应用到 S 中的源码。

源码目录创建见 “获取源码”。补丁 创建和处理见 “修补代码”,也可参阅 “使用 devtool modify 修改已有组件源码” 及 “使用传统内核开发流程修补内核”。

3 配置、编译和暂存

打补丁后,执行配置和编译,再将文件复制到暂存区域,准备打包:

../_images/configuration-compile-autoreconf.svg

涉及任务如下:

  • do_prepare_recipe_sysroot:在 WORKDIR 设置两个系统根目录, 使后续 configure、compile 等任务能访问依赖配方生成的库、头文件等。

    • recipe-sysroot:目标库、头文件及交叉构建需要的其他数据。

    • recipe-sysroot-native:主机原生可执行程序、库和数据,供构建时直接运行。

  • do_configure:启用或禁用软件构建配置选项,来源可为配方、 继承类,软件本身也可能按目标自动配置。

    此任务处理的是配方所构建源码的配置。

    使用 autotools* 时,可通过 EXTRA_OECONF 或 PACKAGECONFIG_CONFARGS 增加选项,类实现见 此处。

  • do_compile:配置完成后编译,在 B 指定目录中进行。 默认 B 与 S 相同。

  • do_install:编译完成后,从 B 复制文件到 D 暂存区, 后续打包使用这些文件。

3 拆分软件包

源码配置、编译和暂存后,分析结果并拆包:

../_images/analysis-for-package-splitting.svg

do_package 和 do_packagedata 分析 D 中的文件, 按可用软件包和文件规则拆分。分析包括拆出调试符号、检查共享库依赖和包关系等。

packagedata 根据分析生成元数据,供创建最终软件包。 do_populate_sysroot 则把安装文件的子集复制到相应系统根目录。 工作、暂存及中间结果使用以下区域:

  • PKGD:拆包前的目的目录,即 package。

  • PKGDESTWORK:package 任务保存包元数据的临时区域,即 pkgdata。

  • PKGDEST:拆包后各包的父目录,即 packages-split。

  • PKGDATA_DIR:保存打包元数据的共享全局目录。内容从 PKGDESTWORK 复制到这里,供全局使用。

  • STAGING_DIR_HOST:组件预期运行系统的系统根目录,即 recipe-sysroot。

  • STAGING_DIR_NATIVE:构建主机组件所用系统根目录,即 recipe-sysroot-native。

  • STAGING_DIR_TARGET:组件在一个系统运行、为另一个机器生成代码时 使用的系统根目录,例如 cross-canadian 配方。

配方输出包列在 PACKAGES, bitbake.conf 默认定义:

PACKAGES = "${PN}-src ${PN}-dbg ${PN}-staticdev ${PN}-dev ${PN}-doc ${PN}-locale ${PACKAGE_BEFORE_PN} ${PN}"

各包有 FILES 定义的默认文件列表。例如 ${PN}-dev 包含开发依赖该 软件的应用所需文件,其默认列表也定义在 bitbake.conf:

FILES:${PN}-dev = "${includedir} ${FILES_SOLIBSDEV} ${libdir}/*.la \
                ${libdir}/*.o ${libdir}/pkgconfig ${datadir}/pkgconfig \
                ${datadir}/aclocal ${base_libdir}/*.o \
                ${libdir}/${BPN}/*.la ${base_libdir}/*.la \
                ${libdir}/cmake ${datadir}/cmake"

这些路径必须是从目标根文件系统看待的 绝对路径,不能引用 D 或 WORKDIR 相关变量。正确示例:

${sysconfdir}/foo.conf

备注

包文件列表使用覆盖语法,以冒号分隔 FILES 和包名。

每个文件只能进入一个包。系统从左到右遍历 PACKAGES,文件进入第一个 FILES 模式匹配的包。

备注

可用 oe-pkgdata-util 查找文件属于哪个包:

$ oe-pkgdata-util find-path '/etc/fstab'
base-files: /etc/fstab

更多信息见 使用 oe-pkgdata-util 查看软件包信息。

要增加 ${PN}-extra 这类任意命名的变体,可加入 PACKAGE_BEFORE_PN:

PACKAGE_BEFORE_PN += "${PN}-extra"

也可用 =+ 前置到 PACKAGES:

PACKAGES =+ "${PN}-extra"

根据 RPM、DEB、IPK 类型,do_package_write_* 创建实际包并放到 ${TMPDIR}/deploy,详见 “软件包源”。

备注

不直接支持从 deploy/* 目录生成正式软件包源。通常需要发行版专用的源 维护机制,把新包上传到正式源,例如 Ångström。因高度依赖发行版,默认 不提供这部分功能。

3 生成镜像

软件包拆分并保存后,BitBake 生成根文件系统镜像:

../_images/image-generation.svg

流程包含多个阶段,依赖多项任务和变量。do_rootfs 创建文件和 目录结构,以下变量帮助确定实际安装列表:

IMAGE_ROOTFS 指向正在创建的文件系统,PACKAGE_INSTALL 提供最终 软件包集合,二者驱动根文件系统生成。

无论目标是否启用包管理,安装都由 dnf/rpm、opkg 或 apt/dpkg 控制。如果 目标不启用,流程末尾会删除包管理数据库。安装最后运行各包的安装后脚本, 无法在主机运行的脚本留到目标首次启动。使用 只读根文件系统 时,全部安装后脚本都必须在主机成功执行。

rootfs 最后执行后处理,包括创建清单和优化。

.manifest 与根文件系统镜像同目录,逐行列出安装包。例如 testimage 类用它 决定是否运行某些测试,详见 IMAGE_MANIFEST。

优化包括 mklibs 及 ROOTFS_POSTPROCESS_COMMAND 指定的其他命令。 mklibs 用于缩小库体积。

根文件系统完成后,do_image 开始镜像处理,先运行 IMAGE_PREPROCESS_COMMAND 列出的函数,然后创建最终输出文件。

系统根据 IMAGE_FSTYPES 动态生成所需 do_image_* 任务,形成一个或 多个镜像文件,并可压缩根文件系统以减小体积。格式由 IMAGE_FSTYPES 决定, 能否压缩取决于格式支持。

动态任务形式为:

do_image_type

例如 ext4 对应:

do_image_ext4

最后执行 do_image_complete,调用 IMAGE_POSTPROCESS_COMMAND 中的函数,对最终输出文件作后处理。

备注

整个镜像生成过程在 Pseudo 下运行,确保根文件系统的文件所有权正确。

3 生成 SDK

构建系统用 BitBake 生成标准 SDK 和 eSDK 的安装脚本:

../_images/sdk-generation.svg

备注

交叉工具链生成见 “交叉开发工具链生成”; 使用 populate_sdk 的优势见 “构建 SDK 安装程序”。

与镜像一样,SDK 生成包含多阶段和变量。do_populate_sdk 和 do_populate_sdk_ext 用关键变量决定实际安装的包,图中变量见 “应用开发 SDK”。

populate_sdk 构建标准 SDK,分目标与主机两部分。目标部分包含面向硬件的库 和头文件,主机部分运行于 SDKMACHINE。

populate_sdk_ext 处理方式不同,它封装完整构建系统,包含主机和目标所需 全部内容。

无论类型,任务在清理后生成交叉环境设置脚本和必要配置。最终输出为包含 环境脚本的交叉工具链安装 .sh 文件。

3 戳记文件与任务重跑

每个任务成功后,BitBake 在 STAMPS_DIR 写入戳记文件。名称开头由 STAMP 决定,末尾包括任务名称和当前 输入校验和。

备注

此命名假定 BB_SIGNATURE_HANDLER 为 OEBasicHash,当前通常如此。

判断是否重跑时,检查是否有输入校验和匹配的戳记。有则假定输出仍存在且有效, 否则重新执行。

备注

戳记机制比 “Setscene 任务与共享状态” 的共享状态缓存更通用:任何有有效戳记的任务都不重跑,不限于可缓存任务。

但戳记只表示工作已完成,不保存输出。实际输出通常位于 TMPDIR,例如配方 WORKDIR。共享状态则增加了缓存输出并跨构建机器共享的能力。

STAMPS_DIR 通常是 TMPDIR 子目录,因此删除 TMPDIR 也会删除戳记,任务便会 正确重跑以重新填充。

若要任务始终视为过期,可设置 nostamp。 依赖它的任务也将始终过期,这未必符合预期。

查看任务签名信息见 “查看任务的变量依赖”。

3 Setscene 任务与共享状态

前文假定需要从头构建,没有预构建对象。实际上,BitBake 可在对象已存在时 跳过任务,这些对象通常由共享状态缓存提供。

备注

相关变量见 SSTATE_DIR 和 SSTATE_MIRRORS。

setscene 即 do_taskname_setscene,是直接安装最终文件而非重新构建的任务变体。 对于 package_write_* 等最终产物很合理;对 patch、unpack 则意义不大,因为 恢复所需工作可能等于或多于原任务。

常见变体对应 package、package_write_*、deploy、packagedata 和 populate_sysroot,基本都是输出最终成果的任务。

系统知道这些任务与前置任务的关系。例如运行 populate_sysroot_setscene 后, 无需再运行 fetch、unpack、patch、configure、compile、install;但若需要 package,则这些前置步骤仍需执行。

若所有内容都来自缓存,有些对象完全不需要。例如无编译和补丁工作就不需要 编译器或 quilt;已有 package_write_* 输出时,也不需要 package 任务数据。

为处理这些情况,BitBake 分两阶段运行。先执行 setscene,快速检查计划目标 的缓存是否存在,而不完整下载。如果都不存在,setscene 阶段结束,进入普通 构建。

找到对象后,从用户最终目标反向分析。例如镜像先检查所需包及镜像工具, 若齐备,连编译器都无需下载。若内容缺失、下载失败或 setscene 失败,再尝试 从缓存安装编译器等依赖。

BB_HASHCHECK_FUNCTION 指定的函数返回可用对象列表。 BB_SETSCENE_DEPVALID 指定的函数判断给定依赖关系是否需要继续追踪, 返回 True 或 False。

备注

可以使用 GPG 签名这些产物,见 共享状态签名。

3 镜像

镜像是可在目标启动的压缩根文件系统,构建输出部分即为镜像。下面展开:

../_images/images.svg

备注

项目提供的示例镜像列表见 “镜像”。

镜像写入构建目录的 tmp/deploy/images/machine/,其中包含预期加载到目标的 文件。DEPLOY_DIR 指向 deploy,DEPLOY_DIR_IMAGE 指向当前配置对应 的镜像目录。

  • kernel-image:内核二进制。KERNEL_IMAGETYPE 决定文件命名,可能 使用不同前缀。机器镜像目录可包含多个内核镜像。

  • root-filesystem-image:目标根文件系统,例如 *.ext3、*.bz2。 IMAGE_FSTYPES 决定格式,同一机器目录可包含多个根文件系统。

  • kernel-modules:包含内核全部已构建模块的归档。此形式用于历史兼容, 可将 MODULE_TARBALL_DEPLOY 设为 0 禁用。机器目录可包含多个归档。

  • bootloaders:若目标适用,包含镜像所需引导加载程序,同一机器可有多个。

  • symlinks:每类文件都有指向最近构建版本的符号链接,方便外部脚本获取最新文件。

3 应用开发 SDK

总体流程图的 Application Development SDK 表示 SDK 输出。eSDK 使用 bitbake -c populate_sdk_ext imagename,标准 SDK 使用 bitbake -c populate_sdk imagename,生成方式有所不同:

../_images/sdk.svg

输出包括自解压安装器 .sh、主机与目标清单及 SDK 测试文件。运行安装器会安装 交叉工具链、库和头文件以及环境设置脚本,建立交叉开发环境。工具链运行在 SDK 机器上,属于主机部分;库和头文件面向目标硬件,属于目标部分。使用工具 前通过环境脚本初始化。

备注

所有输出写入构建目录的 deploy/sdk。配置变量随 SDK 类型不同而不同。 eSDK 相关变量包括:

标准 SDK 相关变量包括:

  • DEPLOY_DIR:指向 deploy。

  • SDKMACHINE:运行交叉工具、为目标硬件创建软件包的机器架构。

  • SDKIMAGE_FEATURES:目标部分包含的功能。

  • TOOLCHAIN_HOST_TASK:主机部分的软件包,运行于 SDKMACHINE。 populate_sdk 有默认包集合,此变量可追加内容。

  • TOOLCHAIN_TARGET_TASK:为目标硬件构建的 SDK 目标部分软件包。

  • SDKPATHINSTALL:安装脚本提供的默认安装路径。

  • SDK_HOST_MANIFEST:主机部分全部已安装包列表,主要用于标准 SDK, 在 eSDK 中也有少量作用。

  • SDK_TARGET_MANIFEST:目标部分全部已安装包列表,同样主要用于标准 SDK,在 eSDK 中也有少量作用。

3 交叉开发工具链生成

创建 交叉开发工具链 时,项目替你 完成大多数工作。本节提供生成和使用的技术背景,另见 Yocto 项目应用开发与软件开发套件(SDK/eSDK)手册。

工具链用于构建运行在目标硬件上的镜像和应用,只需少量命令,构建系统便会 创建所需工具链。

下图展示构建环境中的工具链生成和使用:

../_images/cross-development-toolchains.svg

多数工作在构建主机完成。BitBake 创建镜像时,用主机 gcc 引导生成 gcc-cross, 使用 Clang 时则为 clang-cross。随后用它编译目标源码,可以将其看作仅供 BitBake 内部使用的自动生成交叉编译器。

标准工具链引导过程如下:

gcc -> virtual/cross-binutils -> linux-libc-headers -> virtual/cross-cc -> libgcc-initial -> virtual/libc -> libgcc -> virtual/compilerlibs
  • gcc:由构建主机或 buildtools 归档提供的 GNU 编译器。

  • virtual/cross-binutils:引导 cross-cc 阶段及构建 C 库头文件所需二进制工具。

  • linux-libc-headers:交叉编译器和 C 库构建需要的头文件。

  • virtual/cross-cc:交叉编译器引导的最后阶段,生成 BitBake 为目标设备 构建镜像时实际使用的编译器。

    它是运行在构建主机的本机工具。

  • libgcc-initial:用于引导 virtual/libc 的初始 GCC 支持库。

  • virtual/libc:C 标准库提供者,例如 GNU C Library。

  • libgcc:最终 GCC 支持库,只有 C 库可供链接后才能构建。

  • virtual/compilerlibs:引导过程产生的运行时库,输出包含目标设备所需库 的二进制内容。

还可以为应用开发生成可重定位 SDK 安装器。运行后安装 gcc-cross-canadian、 clang-cross-canadian、binutils-cross-canadian 和其他 nativesdk-* 工具, 它们原生运行于 SDK_ARCH,用于交叉编译和测试软件。图中展示生成 命令。该工具链运行于 SDKMACHINE,它可能与构建主机相同,也可能不同。

备注

如果目标架构受支持,可以使用项目附带的预构建镜像,其中已有交叉开发 工具链安装器。

可重定位工具链的引导过程为:

gcc -> virtual/cross-binutils -> linux-libc-headers -> virtual/cross-cc -> libgcc-initial -> virtual/libc -> gcc-cross-canadian/clang-cross-canadian

除最后一项外,与标准工具链相同。gcc-cross-canadian 或 clang-cross-canadian 是最终可重定位交叉编译器,在 SDKMACHINE 运行并生成目标可执行代码。每种 架构只生成一个,通过编译命令参数选择不同处理器优化,从而避免多个编译器, 减小工具链体积。

备注

eSDK 不使用 cross-canadian,因为它包含构建系统副本,内部系统根目录 已有 gcc-cross。

备注

SDK 使用 Clang 的方法见 PREFERRED_TOOLCHAIN_SDK。

备注

构建工具链安装器的优势见 “构建 SDK 安装程序”。

3 共享状态缓存

构建系统按设计从头构建全部内容,除非 BitBake 判断某部分无需重建。从头 构建保证所有内容都是新的,不受陈旧数据影响。因此,遇到问题时开发者常回到 干净构建,确保从已知状态开始。

这既是优点也是缺点:状态明确且内容最新,但耗时更长,通常重复构建了无需 重建的部分。

共享状态代码支持增量构建,解决三个根本问题:

  • 哪些部分变化,哪些没有?

  • 如何移除和替换已变化的软件?

  • 如何利用可用且无需重建的预构建组件?

第一个问题通过任务输入校验和或签名解决,签名变化便假定输入改变,需要重跑。 第二个问题通过记录每个任务加入了哪些输出,支持删除、升级或其他操作。 第三个问题部分依赖第二项,只要能从远程获取被判定有效的共享状态对象并安装。

备注

下文详述架构、签名和共享状态。

3 总体架构

BitBake 按任务而非配方判断需要构建的部分。例如从 IPK 切换到 DEB 时,install 和 package 输出仍有效,但按配方判断无法生成缺少的 .deb,只能让整个构建 失效重跑。这不是理想方案,而且核心需知道每种任务细节,不易扩展,用户也 难以在不修改打包暂存核心的情况下,从层或外部配方加入新任务。

3 校验和(签名)

共享状态以任务输入的唯一校验和判断是否重跑,因此必须识别全部输入。Shell 任务较容易:每个任务都会生成 run 脚本,对其计算校验和即可反映数据变化。

但有些内容不应纳入,例如具体工作路径 WORKDIR。工作目录变化不应 影响目标包输出,且系统也希望本机和交叉包能够重定位。

备注

本机包和交叉包都在构建主机运行,但交叉包生成目标架构输出。

因此需排除 WORKDIR。简单做法是先将其设为固定值,再计算 run 脚本校验和。

run 脚本还可能包含不会调用的函数。增量构建代码分析 Shell 函数依赖,将 脚本裁剪为最小集合,既解决干扰,也使脚本更易读。

Python 任务采用相同思路,但分析更难,需要识别函数访问的变量和调用的函数。 工具先计算这些依赖,再为实际任务输入数据生成校验和。

类似 WORKDIR,有时应忽略某些依赖,例如:

PACKAGE_ARCHS[vardepsexclude] = "MACHINE"

这确保 PACKAGE_ARCHS 即使引用 MACHINE,也不依赖其值。

反过来,可手动添加 BitBake 无法识别的依赖:

PACKAGE_ARCHS[vardeps] = "MACHINE"

这显式将 MACHINE 加为 PACKAGE_ARCHS 的依赖。

例如,内联 Python 可能导致依赖推断失败。使用 -DDD 调试时,BitBake 会输出 无法确定依赖的情况。团队目前尚未覆盖所有这种依赖,并意识到需要继续解决。

直接输入生成的结果在代码中称 basehash。但还需考虑构建目录中已生成的间接 输入:任务签名应结合其依赖任务的哈希。具体选择哪些依赖属于策略,但最终 效果是合并 basehash 与依赖哈希。

代码层面有多种影响方式。在配置中可为 BitBake 提供额外信息,例如全局排除 永不进入校验和的变量:

BB_BASEHASH_IGNORE_VARS ?= "TMPDIR FILE PATH PWD BB_TASKHASH BBPATH DL_DIR \\
    SSTATE_DIR THISDIR FILESEXTRAPATHS FILE_DIRNAME HOME LOGNAME SHELL TERM \\
    USER FILESPATH STAGING_DIR_HOST STAGING_DIR_TARGET COREBASE PRSERV_HOST \\
    PRSERV_DUMPDIR PRSERV_DUMPFILE PRSERV_LOCKDOWN PARALLEL_MAKE \\
    CCACHE_DIR EXTERNAL_TOOLCHAIN CCACHE CCACHE_DISABLE LICENSE_PATH SDKPKGSUFFIX"

这里未列 WORKDIR,因为它基于已排除的 TMPDIR 构造。

沿依赖链选择哈希的规则更复杂,通常由 Python 函数完成。meta/lib/oe/sstatesig.py 展示两个例子,也说明如何插入自定义策略。它定义 OE-Core 的 OEBasic 与 OEBasicHash。BitBake 默认启用空操作 noop,保持旧行为;OE-Core 则通过 bitbake.conf 默认选择:

BB_SIGNATURE_HANDLER ?= "OEBasicHash"

OEBasicHash 与 OEBasic 基本相同,但把任务哈希加入 戳记文件。 因此影响哈希的元数据变化会自动触发重跑,无需通过递增 PR 触发,变化会自动 传播到构建中。

签名生成器还让以下信息可用于构建:

  • BB_BASEHASH:task-taskname:配方中每个任务的基础哈希。

  • BB_BASEHASH_filename:taskname:各依赖任务的基础哈希。

  • BB_TASKHASH:当前运行任务的哈希。

3 共享状态

校验和及依赖只解决了一半问题,另一半是如何在构建中利用这些信息,复用或 重建具体组件。

sstate 通用地实现任务快照捕获。构建过程并不关心输出 刚刚构建而来,还是从别处下载解包,只需结果可用。

输出分两类:一类只在 WORKDIR 创建目录,例如 install 或 package;另一类 将一组数据并入共享目录树,例如系统根目录。

实现细节尽量隐藏在 sstate 类中。对用户而言,为任务加入共享状态支持可以 像 deploy 类中的以下示例一样简单:

DEPLOYDIR = "${WORKDIR}/deploy-${PN}"
SSTATETASKS += "do_deploy"
do_deploy[sstate-inputdirs] = "${DEPLOYDIR}"
do_deploy[sstate-outputdirs] = "${DEPLOY_DIR_IMAGE}"

python do_deploy_setscene () {
    sstate_setscene(d)
}
addtask do_deploy_setscene
do_deploy[dirs] = "${B}"
do_deploy[cleandirs] = "${DEPLOYDIR}"
do_deploy[stamp-extra-info] = "${MACHINE_ARCH}"

说明如下:

  • 将 do_deploy 加入 SSTATETASKS,便会在该任务前后加入 sstate 类实现的 必要处理。

  • sstate-inputdirs 声明正常执行时将输出写入 DEPLOYDIR,这些输出随后成为 缓存输入。

  • sstate-outputdirs 使缓存内容复制到 DEPLOY_DIR_IMAGE。

    备注

    缓存没有该任务,或输入签名已改变时,先执行 deploy 填充缓存,再复制 到 ${DEPLOY_DIR_IMAGE}。若缓存存在且签名有效,则由 do_deploy_setscene 直接复制,跳过 deploy。

  • 以下任务定义使前述设置实际生效:

    python do_deploy_setscene () {
        sstate_setscene(d)
    }
    addtask do_deploy_setscene
    

    sstate_setscene() 读取标志,可能时通过缓存加速任务。成功返回 True, 否则返回 False 并运行普通任务。详见 “Setscene”。

  • do_deploy[dirs] 在任务前创建 B,并将其设为当前目录;已存在则保留。 见 “Variable Flags”。

  • do_deploy[cleandirs] 在任务前创建空 DEPLOYDIR;已存在则删除并重建为空。 同样详见变量标志章节。

    备注

    输入和输出目录相同时可用 sstate-plaindirs。例如保留 package 的 ${PKGD} 和 ${PKGDEST}:

    do_package[sstate-plaindirs] = "${PKGD} ${PKGDEST}"
    
  • stamp-extra-info 向戳记追加 MACHINE_ARCH,使任务与机器架构关联。 标志详见 “The Task List”。

  • 输入和输出也可包含多个目录。例如 PKGDESTWORK 与 SHLIBWORK 为输入, PKGDATA_DIR 与 SHLIBSDIR 为对应输出:

    do_package[sstate-inputdirs] = "${PKGDESTWORK} ${SHLIBSWORKDIR}"
    do_package[sstate-outputdirs] = "${PKGDATA_DIR} ${SHLIBSDIR}"
    
  • 文件增删敏感时,也支持操作共享状态目录时持有锁文件:

    do_package[sstate-lockfile] = "${PACKAGELOCK}"
    

内部会在 SSTATE_DIR 和 SSTATE_MIRRORS 查找文件,例如:

SSTATE_MIRRORS ?= "\
    file://.* https://someserver.tld/share/sstate/PATH;downloadfilename=PATH \
    file://.* file:///some/local/dir/sstate/PATH"

备注

缓存按哈希前两个字符分为两字符子目录。镜像站若采用相同结构,URI 必须 包含 PATH,才能映射到正确子目录。

文件名包含任务签名,因此只看名称就可判断有效性。找到有效包后,下载并用于 加速任务。

主执行阶段之前,BitBake 通过 *_setscene 尽可能恢复缓存。缓存可用时,原任务 及其依赖无需执行。

例如构建 IPK 镜像时,理想情况下只获取并解包 package_write_ipk 的缓存, 不使用的系统根目录甚至不必提取。这也是任务级方案优于配方级方案的原因, 后者不得不安装每项任务输出。

3 哈希等价

前文说明 BitBake 如何在共享状态已有输出时跳过任务。

构建中,输入变化并不总导致输出变化。例如 C 源码只改了空白,结果仍相同。 项目将这种情况称为“等价”。

为跟踪等价性,每项任务需管理三个哈希:

  • 任务哈希:从配方元数据、任务代码及依赖任务哈希计算。输入变化使哈希 改变,触发重跑,也使下游任务哈希变化,可能沿整条链重新执行。

  • 输出哈希:对保存输出到共享状态归档的任务结果计算。任务哈希与输出哈希 的映射上报给 哈希等价服务器,由数据库保存供以后查询。

  • unihash:初始等于任务哈希,用于跟踪输出的唯一性,具体维护方式如下。

启用哈希等价后,计算任务哈希时使用依赖的 unihash,而非其任务哈希。

设某共享状态任务的代码、元数据或依赖变化,它的任务哈希变化,因而重跑并 重新计算输出哈希。

新的任务哈希与输出哈希映射上报服务器。服务器告知 BitBake:该输出是否 曾由另一个任务哈希产生过。

如果已知,BitBake 将 unihash 更新为最初生成该输出的任务哈希。下游任务 因此保留旧的已知任务哈希,可以从共享状态直接恢复,无需重跑。更下游任务 同样可复用。

如果未知,服务器建立新映射。下游任务仍有变化后的新哈希,因此按预期重跑, 变化继续传播。

因此,某任务变化不一定引发整个下游依赖链重建;只要输出与记录相同,便可 安全恢复后续缓存。

备注

可重现构建 是输出哈希稳定的重要条件,哈希 等价效果强烈依赖它。

不可复现配方可能出现意外行为:服务器可能将不同输出映射回旧缓存。若 差异仅为时间戳等细节,通常无碍;若文件名完全不同等变化较大,下游缓存 实际不等价,就很可能直接失败。

适用场景包括:

  • 不影响输出的细微配方修改,如空白、未使用代码路径或变量顺序变化。

  • 共享库更新,例如安全修复。使用者应重新构建,但新二进制可能完全相同。 库依赖哈希变化使相应任务的输出哈希不同,但实际输出相同可让等价机制 阻止继续向下传播。

  • 本机工具更新。依赖任务需要重建,但很可能输出不变,被判为等价。

Poky 默认启用,由三个变量控制:

Poky 默认相当于:

BB_HASHSERVE = "auto"
BB_SIGNATURE_HANDLER = "OEEquivHash"

也可不用本地服务,而直接使用网络服务器:

BB_HASHSERVE = "<HOSTNAME>:<PORT>"

服务设置见 设置哈希等价服务器。

简明实现介绍可观看 Joshua Watt 在 ELC 2020 的 演讲视频 及 Hash Equivalence and Reproducible Builds 幻灯片。

3 自动添加运行时依赖

系统自动添加常见运行时依赖,无需手写 RDEPENDS。三个机制 shlibdeps、pcdeps、 depchains 分别处理共享库、pkg-config 模块和 -dev/-dbg 包。其他依赖仍需 手动声明。

  • shlibdeps:每个配方的 package 任务查找所安装共享库,将所在包登记为该 库的 soname 提供者。packagedata 将映射保存到 全局 PKGDATA_DIR。

    同时检查所有可执行文件和共享库链接了哪些库。对每项依赖,查询映射, 找到包含该库的包,通常来自其他配方,然后添加到运行时依赖。

    自动依赖还限制版本,至少使用当前库提供包版本,相当于 RDEPENDS 添加 "package (>= version)",必要时强制同步升级库包。

    如果库仅内部使用、不应登记为提供者,在配方的 PRIVATE_LIBS 中列出。

  • pcdeps:package 查找所有 .pc 模块,将所在包登记为提供者,由 packagedata 保存全局映射。

    同时检查模块的 Requires:,识别对其他模块的依赖,再查询 PKGDATA_DIR, 找到提供包并添加运行时依赖。

    备注

    该机制最常推断 -dev 包之间的依赖。

  • depchains:若 foo 依赖 bar,则 foo-dev 依赖 bar-dev,foo-dbg 依赖 bar-dbg。 例如 bar-dev 可能提供 foo-dev 所需头文件和共享库符号链接。

    此机制添加的是 RRECOMMENDS。

    备注

    foo-dev 默认还以 RDEPENDS 依赖 foo,因为 bitbake.conf 的 RDEPENDS:${PN}-dev 包含 ${PN}。

    为避免依赖链断裂,默认即使为空也生成 -dev 和 -dbg,见 ALLOW_EMPTY。

package 通过 [deptask] 依赖 DEPENDS 各配方的 packagedata。因此只要 DEPENDS 正确,所需库和模块 映射就会及时可用。

3 Fakeroot 与 Pseudo

install、package_write*、rootfs 和 image_* 等任务,若能执行通常需要 root 的操作,会更容易实现。例如 install 需要将文件 UID、GID 设为任意值。

可以要求 BitBake 以 root 运行,但这既麻烦又不安全。实际采用“虚假 root” 环境,让任务及子进程认为自己是 root,并看到内部一致的文件系统视图。 只要生成最终包或镜像本身不需要真实 root,前面使用这种环境就不会有问题。

此能力称为 fakeroot,名称来自请求该环境的 BitBake 关键字或变量标志。

构建系统用 Pseudo 实现。它通过 LD_PRELOAD 覆盖系统调用,模拟 root,并用 SQLite 3 数据库记录虚假所有者 和权限。各配方数据库位于 ${WORKDIR}/pseudo/files.db。 采用文件而非内存,使状态跨任务和构建持久保存,这是普通 fakeroot 无法做到的。

备注

自定义任务若操作与 fakeroot 任务相同的文件,也必须在 fakeroot 下运行, 否则既不能执行 root 专用操作,也看不到模拟权限。同时应依赖 virtual/fakeroot-native:do_populate_sysroot,例如:

fakeroot do_mytask () {
    ...
}
do_mytask[depends] += "virtual/fakeroot-native:do_populate_sysroot"

更多信息见 BitBake 的 FAKEROOT* 变量,以及 Why Not Fakeroot?。

3 BitBake 任务图

以下配方和图示展示 BitBake 如何在构建目录和环境中工作,完整呈现生成最终 软件包需要的任务。

示例有两个配方:

  • libhello:提供共享库。

  • sayhello:使用该库完成工作。

备注

sayhello 编译时需要共享库完成动态链接,运行时也需要加载器找到它。 依赖详见 依赖关系。

libhello 源码包括:

  • LICENSE:库的许可证。

  • Makefile:make 构建库使用的文件。

  • hellolib.c:库实现。

  • hellolib.h:C 头文件。

sayhello 源码包括:

  • LICENSE:项目许可证。

  • Makefile:make 构建项目使用的文件。

  • sayhello.c:项目源码。

展示内容前,先列出将 sayhello 集成到根文件系统所需步骤:

  1. 为每个项目创建包含对应文件的 Git 仓库。

  2. 分别创建配方。

  3. 确保 sayhello 的 DEPENDS 包含 libhello。

  4. 确保 sayhello 的 RDEPENDS 包含 libhello。

  5. 将 sayhello 加入 IMAGE_INSTALL。

libhello/Makefile 内容为:

LIB=libhello.so

all: $(LIB)

$(LIB): hellolib.o
   $(CC) $< -Wl,-soname,$(LIB).1 -fPIC $(LDFLAGS) -shared -o $(LIB).1.0

%.o: %.c
   $(CC) -c $<

clean:
   rm -rf *.o *.so*

备注

创建共享库时强烈建议遵循 Linux 惯例,背景见 共享库说明。

备注

Makefile 强烈建议使用 CC、LDFLAGS 和 CFLAGS,BitBake 会根据配置通过 环境变量设置它们。

libhello/hellolib.h 内容为:

#ifndef HELLOLIB_H
#define HELLOLIB_H

void Hello();

#endif

libhello/hellolib.c 内容为:

#include <stdio.h>

void Hello(){
   puts("Hello from a Yocto demo \n");
}

sayhello/Makefile 内容为:

EXEC=sayhello
LDFLAGS += -lhello

all: $(EXEC)

$(EXEC): sayhello.c
   $(CC) $< $(LDFLAGS) $(CFLAGS) -o $(EXEC)

clean:
   rm -rf $(EXEC) *.o

sayhello/sayhello.c 内容为:

#include <hellolib.h>

int main(){
   Hello();
   return 0;
}

libhello_0.1.bb 内容为:

SUMMARY = "Hello demo library"
DESCRIPTION = "Hello shared library used in Yocto demo"

# NOTE: Set the License according to the LICENSE file of your project
#       and then add LIC_FILES_CHKSUM accordingly
LICENSE = "CLOSED"

# Assuming the branch is main
# Change <username> accordingly
SRC_URI = "git://github.com/<username>/libhello;branch=main;protocol=https"

do_install(){
   install -d ${D}${includedir}
   install -d ${D}${libdir}

   install hellolib.h ${D}${includedir}
   oe_soinstall ${PN}.so.${PV} ${D}${libdir}
}

sayhello_0.1.bb 内容为:

SUMMARY = "SayHello demo"
DESCRIPTION = "SayHello project used in Yocto demo"

# NOTE: Set the License according to the LICENSE file of your project
#       and then add LIC_FILES_CHKSUM accordingly
LICENSE = "CLOSED"

# Assuming the branch is main
# Change <username> accordingly
SRC_URI = "git://github.com/<username>/sayhello;branch=main;protocol=https"

DEPENDS += "libhello"
RDEPENDS:${PN} += "libhello"

do_install(){
   install -d ${D}${bindir}
   install -m 0700 sayhello ${D}${bindir}
}

将配方放入自定义层后,运行 bitbake sayhello 即可构建。

下图展示 BitBake 完成此工作的任务顺序:

../_images/bitbake_tasks_map.svg