14 构建

本节介绍简单构建、多配置目标构建、为多台机器生成镜像等各种构建流程。

14 构建简单镜像

在开发环境中,修改硬件支持、添加或修改系统库,以及更改有依赖关系的服务时, 都需要重新构建镜像。Yocto Project 支持多种构建方式,本节介绍在 Linux 构建主机上用 BitBake 构建简单镜像的基本步骤。

备注

构建过程从源码创建完整 Linux 发行版,放在 Build Directory 的 tmp/deploy/images 下。详细流程见概述与概念手册的 “镜像”。

下图和列表概述构建过程:

../_images/bitbake-build-flow.png
  1. 准备开发主机: 设置能够使用 Yocto Project 的构建主机,见 “准备使用 Yocto Project”。

  2. 确认配置正确: 使用 bitbake-config-build 定义 MACHINE 或 DISTRO,在 build/conf/site.conf 中设置站点专用选项。

  3. 构建镜像: 使用 bitbake:

    $ bitbake target
    

    备注

    BitBake 的更多信息见 BitBake User Manual。

    target 是要构建的配方名称。常用目标是 OpenEmbedded-Core (OE-Core) 的 meta/recipes-core/images、meta/recipes-sato/images 等目录中的 镜像,也可以是 BusyBox 等特定软件的配方。支持的镜像详见参考手册的 “镜像” 一章。

    例如,构建 core-image-minimal:

    $ bitbake core-image-minimal
    

    镜像构建完成后,通常还需安装。生成的镜像和内核位于 Build Directory 的 tmp/deploy/images。运行 qemux86、 qemuarm 等预构建镜像的方法见 Yocto 项目应用开发与软件开发套件(SDK/eSDK)手册;安装方法见 对应开发板或机器的文档。

14 通过多配置为多个目标构建镜像

参阅 Yocto Project 开发任务手册的 使用 Multiconfig 为多个目标构建镜像。

14 构建初始 RAM 文件系统(Initramfs)镜像

初始 RAM 文件系统(Initramfs )提供用于早期系统初始化的临时根文件 系统,通常包含定位并挂载最终根文件系统所需的工具和模块。

创建步骤如下:

  1. 创建 Initramfs 镜像配方: 可参考 OpenEmbedded-Core (OE-Core) 的 meta/recipes-core 中的 core-image-minimal-initramfs.bb。 该配方基于下文介绍的 initramfs-framework。

  2. 决定是否将 Initramfs 嵌入内核镜像: 如果需要,在机器等 configuration file 中将 INITRAMFS_IMAGE_BUNDLE 设为 "1",并在内核配方中设置 INITRAMFS_IMAGE。

    设置 INITRAMFS_IMAGE_BUNDLE 后,Initramfs 会解包到 ${B}/usr/,再通过 CONFIG_INITRAMFS_SOURCE 传给内核 Makefile,按正常方式编入内核。

  3. 按需通过配方添加内容: 向 Initramfs 添加内容时,应使用 PACKAGE_INSTALL,而不是 IMAGE_INSTALL。前者能更直接地 控制镜像内容,避免引入 image 或 core-image 类设置但未必需要的默认内容。

  4. 构建内核和 Initramfs: 用 BitBake 构建内核。由于 Initramfs 配方是内核镜像的依赖,也会同时构建;如果设置了 INITRAMFS_IMAGE_BUNDLE,则会嵌入内核镜像。

14 使用 initramfs-framework 定制 Initramfs

meta/recipes-core/images 中的 core-image-minimal-initramfs.bb 使用 initramfs-framework_1.0.bb 作为基础组件。该配方旨在提供构建自定义 Initramfs 的基本构件。

它使用 meta/recipes-core/initrdscripts/initramfs-framework 中的 Shell 初始化脚本。由于部分脚本并非适合所有场景,配方拆分为多个软件包:

  • initramfs-framework-base:安装 init 脚本、/dev/console 字符 特殊文件等基础组件。下列模块都运行时依赖它,因此安装任一模块都会自动引入。

  • initramfs-module-exec:支持执行应用程序。

  • initramfs-module-mdev:支持 mdev。

  • initramfs-module-udev:支持 Udev。

  • initramfs-module-e2fs:支持 ext4/ext3/ext2 文件系统。

  • initramfs-module-nfsrootfs:支持通过 NFS 定位并挂载根分区。

  • initramfs-module-rootfs:支持定位并挂载根分区。

  • initramfs-module-debug:提供动态调试支持。

  • initramfs-module-lvm:支持 LVM 根文件系统。

  • initramfs-module-overlayroot:在只读根文件系统上挂载可读写覆盖层。

除上述配方自身定义的软件包外, meta/recipes-core/initrdscripts 中的配方还提供:

  • initramfs-module-install:在所选块设备上创建并安装分区布局。

  • initramfs-module-install-efi:在所选块设备上创建并安装 EFI 分区布局。

  • initramfs-module-setup-live:如果 内核命令行 传入 root=/dev/ram0 或没有 root=,在 Initramfs 中启动 Shell。

定制时,可为 core-image-minimal-initramfs 创建 bbappend, 从 PACKAGE_INSTALL 添加或移除上述软件包;也可以参考该配方创建 自己的 Initramfs 镜像配方。

通过自编配方可向 Initramfs 添加自定义脚本。惯例名称为 initramfs-module-<module name>,其中 <module name> 是模块名称。 配方的包专用 RDEPENDS 应包含 initramfs-framework-base 及其他 运行时依赖。

配方必须将 Shell 初始化脚本安装到 ${D}/init.d,名称采用 <number>-<script name>:

  • <number> 为 两位数字,影响脚本执行顺序。例如,80-setup-live 在 01-udev 之后执行,因为 80 大于 01。

    必须使用两位数字,因为脚本按字典顺序执行。10-script 会排在 8-script 之前,因此后者应命名为 08-script。

  • <script name> 可自由选择。

    如果数字相同,则按脚本名称的字典顺序排序。

14 嵌入来自独立 Multiconfig 的 Initramfs 镜像

有时需要 Initramfs 不继承主镜像的发行版策略。例如,主镜像使用 TCLIBC = "glibc",但 Initramfs 使用 TCLIBC = "musl" 以减小体积。 仅按上述步骤操作,Initramfs 会继承 glibc 设置,无法覆盖。

为此,需要额外执行:

  1. 为 Initramfs 创建 multiconfig: 按 “通过多配置为多个目标构建镜像” 创建独立配置。假设名为 initramfscfg.conf,内容如下:

    TMPDIR = "${TOPDIR}/tmp-initramfscfg"
    TCLIBC = "musl"
    
  2. 在主配置设置额外 Initramfs 变量: 在 configuration file 中添加:

    INITRAMFS_MULTICONFIG = "initramfscfg"
    INITRAMFS_DEPLOY_DIR_IMAGE = "${TOPDIR}/tmp-initramfscfg/deploy/images/${MACHINE}"
    

    INITRAMFS_MULTICONFIG 和 INITRAMFS_DEPLOY_DIR_IMAGE 在内核与 initramfscfg 中的 INITRAMFS_IMAGE 之间建立多配置 依赖,并指定该镜像的位置。

    此配置下,内核仍用主配置构建,但 do_bundle_initramfs 从 INITRAMFS_DEPLOY_DIR_IMAGE 获取指定 INITRAMFS_IMAGE。 最终内核嵌入基于 musl 的 Initramfs,主镜像则基于 glibc。

    同一方法也可避免 Initramfs 继承 DISTRO_FEATURES,或为其使用 poky-tiny 等不同 DISTRO。

14 构建精简系统

极小发行版具有明显优势:片上或封装内存需求更低、成本更低;缓存利用更有效, 性能更好;内存更少,功耗更低;启动更快,开发开销更小。数码相机、医疗设备和 小型无显示器系统等场景尤其受益。

本节介绍如何将发行版裁剪到比 Yocto Project 开箱可构建、约 5 MB 的 poky-tiny 更小。

14 精简系统概述

为缩小根文件系统、加快启动、保留关键功能并避免初始 RAM 磁盘,需要考虑:

14 目标和指导原则

动手前先明确目标。创建极小发行版时,可参考以下原则:

  • 确定空间目标,例如内核不超过 1 MB,根文件系统不超过 3 MB。

  • 找出占用 90% 空间的部分,集中优化。

  • 不要为达到目标引入难以维护的特殊改造。

  • 利用设备专用选项。

  • 在独立层中工作以隔离变更。创建层的方法见 “理解和创建层”。

14 了解镜像体积的组成

创建发行版时最好有现成起点。可以直接用 Yocto Project 创建 poky-tiny, 最终再以其为参考,在自己的发行版中修改。

备注

使用 poky-tiny 时,按 bitbake-config-build enable-fragment 将 distro/ 片段设为 "poky-tiny",或在 build/conf/local.conf 中将 DISTRO 设为该值。

理解内存概念有助于缩小系统。内存分为静态、动态和临时部分:静态部分包括 TEXT(代码)、DATA(已初始化数据)和 BSS(未初始化数据);动态部分包括 运行时分配的栈、哈希表等;临时部分在启动后回收,包括内核解压缩及 __init__ 函数使用的内存。

OpenEmbedded-Core (OE-Core) 的 scripts/tiny/ 中有两个工具, 用于查看内核和根文件系统当前大小:

  • ksize.py:报告内核构建对象各组件的大小。

  • dirsize.py:报告根文件系统各组件的大小。

以下工具和命令用于组织配置片段,并直观查看文件依赖:

  • merge_config.sh:管理内核配置文件和片段,可将独立片段合并,允许 覆盖设置并对缺失选项发出警告。适合迭代配置、创建最小配置,以及为不同 机器生成配置,无需重复整个流程。

    此脚本位于 Linux Yocto 内核仓库(如 linux-yocto-3.14、 linux-yocto-3.10、linux-yocto-3.8)的 scripts/kconfig。

    配置片段详见内核开发手册的 “创建配置片段”。

  • bitbake -u taskexp -g bitbake_target:打开依赖浏览器,查看文件依赖。 理解依赖有助于合理决定裁剪哪些内核和根文件系统内容。

14 裁剪根文件系统

根文件系统由启动组件、库和应用软件包组成。可以调整打包配置来改变构建方式, 也可以修改文件系统本身或选择另一种文件系统。

先在根目录运行 dirsize.py,查明空间主要被什么占用:

$ cd root-directory-of-image
$ dirsize.py 100000 > dirsize-100k.log
$ cat dirsize-100k.log

脚本可过滤小于指定大小的文件,上例忽略小于 100 KB 的文件。报告的是未压缩 大小,在压缩根文件系统中通常会按相对固定比例缩小。检查日志时,重点关注 占用大量空间的区域。

确保裁剪不会破坏所需功能。可以通过 BitBake 的依赖浏览器观察软件包关系:

$ cd image-directory
$ bitbake -u taskexp -g image

在界面中选择候选删除软件包,查看其依赖关系。

优先移除对功能集影响最小的软件包。例如,可能不需要 VGA 显示,或可以用 devtmpfs 和 mdev 替代 udev。

在发行版 configuration file 中进行此类修改。例如,移除 udev 和 glib:

VIRTUAL-RUNTIME_dev_manager = ""

最后,选择既满足需求又能减小体积的根文件系统,例如 cramfs、squashfs、 ubifs、ext2 或基于 initramfs 的 Initramfs。注意, ext3 需要 1 MB 日志;如果可以只读运行,就不需要该日志。

备注

每轮裁剪后都需重新构建,再使用工具检查效果。

14 裁剪内核

内核构建包含与硬件无关的策略:启用哪些子系统、面向哪种架构、默认构建哪些驱动?

备注

如果希望改善启动时间,也可以修改内核源码。

在 Linux 构建顶层目录运行 ksize.py,了解内核组成:

$ cd top-level-linux-build-directory
$ ksize.py > ksize.log
$ cat ksize.log

日志显示驱动、网络、内核核心、文件系统、声音等内置 .o 文件的空间占用。 这些值未压缩,压缩内核镜像中通常按相对固定比例缩小。重点处理占用较大、符合 “90% 原则”的区域。

使用 -d 深入查看各区域:

$ ksize.py -d > ksize.log

该选项展开驱动、网络等区域的各个文件信息。

结合可以舍弃的功能和日志决定裁剪内容,例如不需要声音就可移除全部声音驱动。

决定后,需要重新配置内核,使下次构建体现变更。可以用 menuconfig 一次 完成所有修改,但难以判断每项裁剪的效果,也不利于为其他目标复现。更好的方式 是从 allnoconfig 开始,为各项变化创建配置片段,再用 merge_config.sh 合并为单个配置文件。这样便于反复进行配置、构建的迭代。

每次修改配置后,都应重建内核并检查对整体体积的影响。

14 移除软件包管理需求

包管理会增加镜像体积。可以移除包管理器、其独有依赖以及包管理数据来缩小镜像。

确保镜像的 IMAGE_FEATURES 中不包含 "package-management",即可从 根文件系统移除包管理器及其依赖。

14 寻找其他缩小体积的方法

根据具体系统,通常还存在其他可裁剪区域。结合上述工具和方法,不断试验和迭代 才能找到它们。可以尝试:

  • glibc:一般流程如下:

    1. 从 DISTRO_FEATURES 移除认为不需要的 glibc 功能。

    2. 构建发行版。

    3. 如果某包因缺少符号而失败,判断能否重新配置该包,使其不依赖这些功能。 例如像 ncurses 一样禁用宽字符支持。如果仍需要,则查明哪些 glibc 功能提供支持,并恢复相应配置。

    1. 重新构建,重复上述流程。

  • busybox:流程与 glibc 类似,但需要启动生成的系统,确认所有预期 操作仍能完成。还必须将配置片段集成到 BusyBox,因为它自行管理核心功能, 再允许在其上添加配置片段。

14 反复迭代

如果尚未达到体积目标,就重复上述流程。用工具找出占根文件系统和内核 90% 的内容,判断哪些可以移除,而不会使设备功能低于实际需要。

BusyBox 常是值得检查的地方,它在单个可执行文件中提供精简版 Unix 工具。 可能还能去掉虚拟终端服务或 IPv6。

14 为多台机器构建镜像

常见需求是为使用相同软件环境的多台机器生成镜像。人们容易为每台硬件选择 最专用的调优和优化标志,但这会显著增加总构建时间和软件包源维护成本。 例如,针对特定 CPU 核心的调优可能启用一些 GCC 微优化,但相比所有构建使用 更通用的设置,实际性能差异未必明显。与其为每台机器分别将 DEFAULTTUNE 等调到极致,不如让构建系统在适合的位置跨机器复用软件。

如果重视构建速度和软件包源维护,可考虑以下做法:

  • 共享 Build Directory : 尽可能让各构建共享 TMPDIR。 Yocto Project 支持在同一临时目录切换 MACHINE,开发者经常这样 进行多机器构建。这样可复用本机配方,通常也能复用交叉配方,从而缩短时间。

    备注

    如果 DISTRO 或文件系统布局等基础配置发生变化,应使用干净的 TMPDIR。共享有时可能可行,但无法保证。

  • 选择适当的软件包架构: 构建系统默认提供 "all"、"tune" 或 "package"、 "machine" 三个层次。配方通常选择其中一种。根据输出选择正确架构,会 直接影响构建时间。

    只生成脚本的配方可以使用 "all",因为无需构建二进制文件。继承 allarch 即可,它会配置多个变量,使软件包跨架构通用。

    如果输出是机器专用的,或构建、运行时依赖已具有机器架构依赖,那么配方 也具有这种依赖,应通过 MACHINE_ARCH 启用 "machine" 架构:

    PACKAGE_ARCH = "${MACHINE_ARCH}"
    

    未通过 PACKAGE_ARCH 显式指定时,默认使用 TUNE_PKGARCH:

    PACKAGE_ARCH = "${TUNE_PKGARCH}"
    
  • 尽可能使用通用调优文件: 某些调优适合多个目标,例如 armv5 软件包 通常可运行于 armv6、armv7,i486 二进制可运行于 i586 及更高处理器,但不会利用新处理器的改进特性。

    多台机器使用相同调优时,构建系统复用已有软件。即使各机器生成独立系统 根目录,也不会重新编译软件,且只需维护一个软件包源。

  • 管理更细粒度的打包: 有时在上述三层之外增加架构层次很有帮助。例如, NXP(原 Freescale)的 meta-freescale 层 通过 fsl-dynamic-packagearch 为 i.MX53 板共享 AMD GPU 软件包,为 i.MX6 板共享 Vivante GPU 软件包。 该类检查 BitBake 数据存储,判断包是否提供或依赖某个子架构值;如果是, 根据 MACHINE_SUBARCH 设置 PACKAGE_ARCH。否则,若命中机器 专用过滤器,则设为 MACHINE_ARCH。通过复用二进制减少构建软件包 数量,节省时间。

  • 使用工具排查问题: 有时软件发生了看似不必要的重建,例如机器间没有 如预期共享状态。通常是本应只与调优相关的代码引用了 MACHINE、 SERIAL_CONSOLES、XSERVER、MACHINE_FEATURES 等 机器变量,或者通过 DEPENDS、RDEPENDS、RRECOMMENDS、 RSUGGESTS 等依赖了 PACKAGE_ARCH 为 "${MACHINE_ARCH}" 的其他配方。

    备注

    此类问题偶尔会出现,欢迎提交修复补丁。

    可用工具包括:

    • state-diff-machines.sh : 位于源码仓库 scripts,用法见脚本注释。

    • BitBake 的 "-S printdiff": 尝试寻找最近的签名匹配,例如共享状态 缓存中的匹配,再比较签名,找出两个戳记树发生分歧的位置和差异。

14 从外部源码构建软件

默认情况下,OpenEmbedded 在 Build Directory 构建源码,先获取、 解包并按需应用补丁,然后进行构建。

有时希望直接构建系统之外的源码。例如,新 BSP 使用高度定制内核,希望开发 团队尽量少接触构建系统,专注项目并保持原有流程。可以在开发机器上保留独立 内核源码目录,让配方的 SRC_URI 指向它并直接使用,而不复制。

实际从外部源码构建,只需继承 externalsrc,将 EXTERNALSRC 指向源码。在 build/conf/local.conf 中设置:

INHERIT += "externalsrc"
EXTERNALSRC:pn-myrecipe = "path-to-your-source-tree"

也可在配方或追加文件中设置 EXTERNALSRC:

EXTERNALSRC = "path"
EXTERNALSRC_BUILD = "path"

备注

必须在全局或局部继承 externalsrc,这些设置才会生效。

默认在 EXTERNALSRC 指定源码目录之外的独立目录构建。如果需要在源码 目录本身或其他指定目录构建,设置 EXTERNALSRC_BUILD:

EXTERNALSRC_BUILD:pn-myrecipe = "path-to-your-source-tree"

备注

EXTERNALSRC 和 EXTERNALSRC_BUILD 必须为绝对路径。

14 离线复现构建

可以为构建所用上游源码制作“快照”,随后离线复现构建。先准备下载目录并填充 快照文件,之后就可随时在任何机器上使用它复现。

填充下载目录的步骤如下:

  1. 创建干净下载目录: 从空 DL_DIR 开始。可以清空已有目录,或将 变量指向一个空位置或尚不存在的位置。

  2. 为源码 Git 仓库生成归档包: 在 build/conf/local.conf 中设置:

    DL_DIR = "/home/your-download-dir/"
    BB_GENERATE_MIRROR_TARBALLS = "1"
    

    下一步获取源码时,BitBake 会收集文件,并在 DL_DIR 中创建归档包。 更多信息见 BB_GENERATE_MIRROR_TARBALLS。

  3. 仅填充下载目录,不构建: 用 BitBake 获取源码,但禁止构建:

    $ bitbake target --runall=fetch
    

    下载目录 ${DL_DIR} 现在包含归档包形式的源码快照,可用于构建。

  4. 按需删除 Git 或其他 SCM 子目录: 可以移除 ${DL_DIR}/git2/* 等 版本控制子目录,因为归档包已经包含这些内容。

所需源码齐备后,即可创建自己的 "own-mirror" 并构建目标。这些文件可以在 任何机器、任何时间用于离线构建。

使用下载目录中的文件构建:

  1. 仅使用本地文件: 在 build/conf/local.conf 或发行版 configuration file 中设置 SOURCE_MIRROR_URL,继承 own-mirrors 并设置 BB_NO_NETWORK:

    SOURCE_MIRROR_URL ?= "file:///home/your-download-dir/"
    INHERIT += "own-mirrors"
    BB_NO_NETWORK = "1"
    

    SOURCE_MIRROR_URL 和 own-mirrors 将下载目录 配置为自有镜像源;BB_NO_NETWORK 确保第三步的获取过程只使用 本地文件。

  2. 从干净构建开始: 删除 ${TMPDIR},或使用新的 Build Directory。

  3. 构建目标: 使用 BitBake:

    $ bitbake target
    

    构建使用自有镜像源中的本地源码快照完成。快照归档包位于下载目录。

    备注

    如果配方将 SRCREV 设为 ${AUTOREV}, 尝试查找最新版本,离线构建将无法工作:

    SRCREV = "${AUTOREV}"
    

    此设置使构建系统访问网络,从 SCM 查询最新版本。使用 AUTOREV 的通常是自定义或修改过的配方,公共仓库配方一般不使用它。

    如果确有此类配方,仍可通过以下步骤离线构建:

    1. 使用启用 构建历史 后生成的配置。

    2. 用 buildhistory-collect-srcrevs 收集构建历史保存的 SRCREV。 详见 “构建历史中的软件包信息”。

    3. 获得正确源码修订后,修改配方,将 SRCREV 固定为具体版本。