许多现成的嵌入式 Linux 发行版都具备良好的功能,但代价是镜像大小,有些镜像可能达到 4GB。有时我们希望嵌入式系统支持 Linux,但只需要系统所需的最小软件包,例如运行一个小型 FTP 服务器,而无需图形界面。

为了实现获取自定义镜像的目标,我们可以通过一些用于嵌入式 Linux 系统的构建工具来自动化和简化发行版的构建。目前的一些现有工具包括 YoctoBuildroot,它们都是开源项目。

在本篇博文中,我们将介绍如何使用 Buildroot 以及如何利用它为 RaspberryPi3 创建自定义镜像。完整的示例代码可在 此处 获取。

Buildroot

Buildroot 是一款用于自动化创建嵌入式 Linux 发行版的工具。它会根据设定的目标板架构构建代码,所有这些都通过 Makefiles 的概览来完成。除了开源之外,它还使用 GPL-2.0-or-later 许可证。

如何安装

在开始 Buildroot 安装之前,假设您已经准备好了一个用于构建 C 项目的 Linux 环境,以及 git、svn 和 rsync 等工具。

您可以通过 Buildroot 官方 文档 获取更多关于需求的信息。

为了安装 Buildroot,我们将通过 Github 克隆存储库

$ git clone https://github.com/buildroot/buildroot.git buildroot

分析 Buildroot

进入 Buildroot 目录后,我们将看到以下目录结构

buildroot/
|
├── arch
├── board
├── boot
├── configs
├── docs
├── linux
├── package
├── support
├── system
├── toolchain
└── utils

每个目录都包含一组用于设置构建一部分所需的文件。在这里,我们可以重点介绍以下内容:

  • board:包含目标板映射和配置的文件,例如闪存地址和设备树文件;
  • configs:包含一系列预设配置,用于自动确定应将哪些软件包和属性添加到嵌入式镜像中;
  • packages:包含迄今为止 Buildroot 可用的所有官方软件包。我们不限于这些软件包,Buildroot 允许我们创建新的自定义软件包。

我们将更多关注 package 文件夹,因为我们的主要兴趣是自定义镜像中安装的软件包。

例如,让我们看看 fmt 软件包,它包含 3 个文件

package/fmt/
|
├── Config.in
├── fmt.hash
└── fmt.mk

Config.in 是用于 Buildroot 配置的软件包描述,它负责在选择要构建的软件包时维护用户界面信息。它还包含软件包依赖项。

config BR2_PACKAGE_FMT
    bool "fmt"
    depends on BR2_INSTALL_LIBSTDCPP
    depends on BR2_USE_WCHAR
    help
      fmt is an open-source formatting library for C++. It can be
      used as a safe alternative to printf or as a fast alternative
      to IOStreams.

      https://fmt.dev/latest/index.html

fmt.mk 文件是用于设置、构建和安装库的 Makefile 规则。

FMT_VERSION = 5.3.0
FMT_SITE = $(call github,fmtlib,fmt,$(FMT_VERSION))
FMT_LICENSE = BSD-2-Clause
FMT_LICENSE_FILES = LICENSE.rst
FMT_INSTALL_STAGING = YES

FMT_CONF_OPTS = \
    -DHAVE_OPEN=ON \
    -DFMT_INSTALL=ON \
    -DFMT_TEST=OFF

$(eval $(cmake-package))

此文件存储所有其他软件包的默认属性,例如其版本、下载源代码的站点、软件许可证名称以及查找该许可证文件的位置。

在这里您可以看到,最后调用了一个名为 cmake-package 的模块。此模块负责处理使用 CMake 的项目,它将执行所有必要的命令,从配置到工件的安装。这种模块化允许更高水平的自动化,否则将需要为每个软件包描述所有 CMake 命令。

最后一个也是不可或缺的文件 fmt.hash 包含从站点直接下载的文件的校验和。

sha256 defa24a9af4c622a7134076602070b45721a43c51598c8456ec6f2c4dbb51c89  fmt-5.3.0.tar.gz
sha256 560d39617dfb4b4e4088597291a070ed6c3a8d67668114ed475c673430c3e49a  LICENSE.rst

虽然我们使用的是 SHA-256,但 Buildroot 能够支持其他格式,如 SHA-1 和 MD5。Buildroot 在下载软件包期间会自动验证校验和。如果找到的值与描述的不相同,则会引发错误。

配置自定义镜像

由于我们的目标平台是 RaspberryPi3,Buildroot 为此板提供了一个预配置的文件,该文件位于 configs 目录中。

要告诉 Buildroot 我们希望从 RaspberryPi3 构建配置,我们应该使用以下命令

$ make raspberrypi3_defconfig

执行此命令后,将生成 .config 文件,其中包含镜像所需的全部软件包、内核、工具链和属性。要添加新的软件包或编辑现有的软件包,我们需要操作此文件,但这并非完全自动化,并且可能导致构建过程中出现许多错误。因此,Buildroot 提供了更友好的界面,您可以在其中自定义最终配置并自动解决依赖关系。此界面有多种不同的格式,您可以尝试其中的一些

$ make config
$ make menuconfig
$ make gconfig
$ make xconfig

在本示例中,我们将使用 menuconfig,因为它具有最小的图形界面,并且不需要 Qt 等其他系统依赖项。

执行配置命令后,我们将获得以下输出

正如我们之前详细介绍的 fmt 库,我们将将其包含在我们的镜像中,因此我们必须按照以下方式浏览菜单

目标软件包 -> 库 -> 文本和终端处理 -> fmt

要获取有关软件包的更多信息,您可以输入 ?。它将显示与我们在 Config.in 文件中看到的相同内容。选择后,我们可以通过面板保存当前设置,然后按 ESC 退出。

构建镜像

设置完成后,我们可以继续本教程中最长的步骤:构建镜像。虽然构建只是一个命令,但 Buildroot 将不得不下载配置文件中存在的所有源代码,从源代码构建,最后生成自定义镜像。要启动构建过程,只需运行

$ make

从现在开始,Buildroot 将负责整个构建过程,第一次构建可能需要几个小时。对于将来的构建,可以使用缓存,这将使构建时间缩短到几分钟。

在 Buildroot 构建中使用 Conan

虽然 Buildroot 可以通过其软件包结构接受新软件包,但构建过程仍然有点长,第一次可能需要几个小时。但是,如果这个过程可以通过下载预构建的软件包缩短到几分钟会怎么样呢?让我们看看 Conan 与我们的场景相关的某些功能和方面

  • 能够提供所有平台和配置的所有二进制文件的统一视图,而不仅仅是 Buildroot;
  • 开发人员可以快速开发,在他们的 Linux 机器上使用本地二进制文件进行本地测试;
  • 更快的构建速度,这要归功于二进制文件的重用,不仅用于开发,还用于生产和发布;
  • 最佳 DevOps 实践,避免多次从源代码重建二进制文件。

在将 Conan 集成到 Buildroot 之前,我们需要了解与 Buildroot 协作构建软件包的脚本结构

$ ls package/*.mk
package/doc-asciidoc.mk   package/pkg-cmake.mk  package/pkg-download.mk  package/pkg-golang.mk
...

此 Makefiles 列表负责为每个给定的软件包执行构建过程。回到 ZLib 库的规则示例,我们有以下部分

$(eval $(cmake-package))

此行告诉我们 pkg-cmake.mk 脚本将用于构建 ZLib 项目。在集成 Conan 的情况下,我们将不得不构建一个新的脚本,其中包含根据 Buildroot 给出的配置下载所需软件包并将其工件复制到其正确位置的命令。

将 Conan 与 Buildroot 集成

让我们在 package/ 目录中创建一个名为 pkg-conan.mk 的新文件。同时,我们需要将其添加到 package/Makefile.in 文件中,以便 Buildroot 能够列出它。

$ echo 'include package/pkg-conan.mk' >> package/Makefile.in

对于脚本开发,我们将将其分解成几个步骤。由于它是一个大文件,我们只会在本文中展示其中的一部分,但完整版本可以在 此处 找到。

Buildroot 定义了其设置,包括处理器、编译器版本和构建类型,通过变量来实现。但是,这些变量对于 Conan 来说没有直接有效的取值,因此我们需要解析其中大部分变量。让我们从编译器版本开始,默认情况下 Buildroot 使用基于 GCC 的工具链,因此我们只会过滤其可能的版本

CONAN_SETTING_COMPILER_VERSION  ?=
ifeq ($(BR2_GCC_VERSION_8_X),y)
CONAN_SETTING_COMPILER_VERSION = 8
else ifeq ($(BR2_GCC_VERSION_7_X),y)
CONAN_SETTING_COMPILER_VERSION = 7
else ifeq ($(BR2_GCC_VERSION_6_X),y)
CONAN_SETTING_COMPILER_VERSION = 6
else ifeq ($(BR2_GCC_VERSION_5_X),y)
CONAN_SETTING_COMPILER_VERSION = 5
else ifeq ($(BR2_GCC_VERSION_4_9_X),y)
CONAN_SETTING_COMPILER_VERSION = 4.9
endif

对于 build_type、arch 等也应该重复此过程。对于 Conan 软件包安装步骤,我们将有以下例程

define $(2)_BUILD_CMDS
    $$(TARGET_MAKE_ENV) $$(CONAN_ENV) $$($$(PKG)_CONAN_ENV) \
        CC=$$(TARGET_CC) CXX=$$(TARGET_CXX) \
        $$(CONAN) install $$(CONAN_OPTS) $$($$(PKG)_CONAN_OPTS) \
        $$($$(PKG)_REFERENCE) \
        -s build_type=$$(CONAN_SETTING_BUILD_TYPE) \
        -s arch=$$(CONAN_SETTING_ARCH) \
        -s compiler=$$(CONAN_SETTING_COMPILER) \
        -s compiler.version=$$(CONAN_SETTING_COMPILER_VERSION) \
        -g deploy \
        --build $$(CONAN_BUILD_POLICY)
endef

Conan install 命令将照常执行,但设置和选项是通过之前从 Buildroot 收集的内容进行配置的,并通过 Buildroot 软件包规则接受新的设置。由于之前所有源代码都是在一开始就编译的,因此我们将 Conan 构建策略设置为 missing,因此如果软件包不可用,则会构建任何软件包。

此外,请注意我们正在使用生成器 deploy,因为我们需要将所有工件复制到 Buildroot 内部结构中。构建完成后,我们将通过以下例程复制库、可执行文件和头文件

define $(2)_INSTALL_CMDS
    cp -f -a $$($$(PKG)_BUILDDIR)/bin/. /usr/bin 2>/dev/null || :
    cp -f -a $$($$(PKG)_BUILDDIR)/lib/. /usr/lib 2>/dev/null || :
    cp -f -a $$($$(PKG)_BUILDDIR)/include/. /usr/include 2>/dev/null || :
endef

使用此脚本,我们将能够安装绝大多数 Conan 软件包,每个 Buildroot 规则只需使用更简单的信息即可。

安装 Conan Zlib

拥有了用于安装 Conan 软件包的脚本后,现在让我们安装一个相当简单且众所周知的项目:zlib。为此,我们将创建一个新的规则文件到 package 目录中。让我们从软件包配置文件开始

mkdir package/conan-zlib
touch package/conan-zlib/Config.in
touch package/conan-zlib/conan-zlib.mk

Config.in 文件的内容应如下所示

config BR2_PACKAGE_CONAN_ZLIB
    bool "conan-zlib"
    help
      Standard (de)compression library. Used by things like
      gzip and libpng.

      http://www.zlib.net

现在让我们转到包含 Zlib 数据的 conan-zlib.mk

# conan-zlib.mk
CONAN_ZLIB_VERSION = 1.2.11
CONAN_ZLIB_LICENSE = Zlib
CONAN_ZLIB_LICENSE_FILES = licenses/LICENSE
CONAN_ZLIB_SITE = $(call github,conan-community,conan-zlib,92d34d0024d64a8f307237f211e43ab9952ef0a1)
CONAN_ZLIB_REFERENCE = zlib/$(CONAN_ZLIB_VERSION)@conan/stable

$(eval $(conan-package))

这里需要注意的是,即使 CONAN_ZLIB_SITE 未用于我们的目的,也需要它。如果不存在,Buildroot 将在其执行期间引发错误。其他变量很简单,只是表示软件包引用、名称、版本和许可证。请注意,最后我们调用了我们的脚本,该脚本应执行 Conan。

创建完成后,我们还需要将其添加到 Buildroot 配置列表中。为此,让我们使用名为 Conan 的新菜单更新列表。在 package/Config.in 文件中,让我们添加以下部分

menu "Conan"
    source "package/conan-zlib/Config.in"
endmenu

现在只需通过 menuconfig 选择软件包即可

目标软件包 -> Conan -> conan-zlib

配置并保存后,只需再次运行 make 即可安装软件包。在安装过程中,我们将看到以下输出

Buildroot 构建

如您所见,Conan 遵循 Buildroot 使用的相同配置文件,这使我们无需手动创建配置文件。

安装结束时,它将被复制到输出目录。

自定义 Conan 远程

假设我们有一个 Artifactory 实例,其中所有软件包都可供下载。我们如何自定义 Buildroot 使用的远程?我们需要引入一个新选项,我们可以在其中写入远程名称,Conan 将能够使用此变量。首先,我们需要创建一个新的配置文件,以在 Conan 的菜单中插入新的选项

$ mkdir package/conan
$ touch package/conan/Config.in

Config.in 文件应包含

config CONAN_REMOTE_NAME
	string "Conan remote name"
    help
	  Look in the specified remote server.

此外,我们需要在 pkg-conan.mk 中解析选项 CONAN_REMOTE_NAME 并将其添加到 Conan 命令行

ifneq ($(CONAN_REMOTE_NAME),"")
CONAN_REMOTE = -r $$(CONAN_REMOTE_NAME)
endif

...

define $(2)_BUILD_CMDS
    $$(TARGET_MAKE_ENV) $$(CONAN_ENV) $$($$(PKG)_CONAN_ENV) \
        CC=$$(TARGET_CC) CXX=$$(TARGET_CXX) \
        $$(CONAN) install $$(CONAN_OPTS) $$($$(PKG)_CONAN_OPTS) \
        $$($$(PKG)_REFERENCE) \
        -s build_type=$$(CONAN_SETTING_BUILD_TYPE) \
        -s arch=$$(CONAN_SETTING_ARCH) \
        -s compiler=$$(CONAN_SETTING_COMPILER) \
        -s compiler.version=$$(CONAN_SETTING_COMPILER_VERSION) \
        -g deploy \
        --build $$(CONAN_BUILD_POLICY) \
        $$(CONAN_REMOTE)
endef

现在我们已准备好设置我们特定的远程名称。我们只需要运行 make menuconfig 并按照以下路径操作

目标软件包 -> 库 -> Conan -> Conan 远程名称

我们将看到

Buildroot 构建

现在 Conan 已配置为在名为 artifactory 的远程服务器上搜索软件包。但是我们需要再次运行 make。请注意,构建将花费更少的时间,因为现在我们拥有 Buildroot 提供的缓存。现在我们已准备好进行最后一步。

安装镜像

经过两个小时和几杯咖啡,如果在此过程中没有发生错误,我们将看到以下输出

$ ls output/images/
  bcm2710-rpi-3-b.dtb bcm2710-rpi-3-b-plus.dtb bcm2710-rpi-cm3.dtb boot.vfat rootfs.ext2 rootfs.ext4 rpi-firmware sdcard.img zImage

$ ls -lh output/images/sdcard.img
    -rw-r--r-- 1 conan conan 153M ago  6 11:43 output/images/sdcard.img

这些工件是构建过程中生成的所有内容的最终编译结果,在这里我们将对 sdcard.img 文件感兴趣。这是我们将用于 RaspberryPi3 的最终镜像,它只有 153MB。与 Raspbian 等其他嵌入式发行版相比,它要小得多。

现在让我们将镜像复制到目标 SD 卡

$ sudo dd if=output/images/sdcard.img of=/dev/mmcblk0 bs=4M conv=sync status=progress

请记住,SD 卡的挂载点可能因您的发行版而异。

完成后,将 SD 卡插入 RaspberryPi3 并为其供电,同时连接到视频输出。您将看到引导加载程序正常运行,最后您将看到登录屏幕,默认用户为 root,无需密码。

结论

在这篇文章中,我们讨论了如何使用 Buildroot 创建 Linux 发行版,而无需太多努力,并且可以生成非常精简的镜像。

Buildroot 有助于自动化创建自定义嵌入式 Linux 发行版的过程,并且只包含开发人员感兴趣的软件包。

虽然构建过程可能需要几个小时,但通过 Conan 集成和替换一些软件包,此时间可以缩短到几分钟。

有兴趣了解更多信息或对此主题发表评论?请随时打开一个新的 问题