最近当我阅读了 Alan Formy-Duval 的文章 使用 Cockpit 管理您的 Raspberry Pi 后,我认为创建一个预装 Cockpit 的镜像会是个好主意。幸运的是,至少有两种方法可以完成这项任务:
- 调整 Raspberry Pi OS 镜像构建工具链 pi-gen 的源代码,这使您能够从零开始构建 Raspberry Pi 镜像
- 将您正在运行的、已修改的 Raspberry Pi OS 转换回其他人可以使用的镜像
本文涵盖了这两种方法。我将重点介绍每种技术的优缺点。
Pi-gen
让我们从 pi-gen 开始。在开始之前,您需要考虑一些先决条件。
先决条件
为了成功运行构建过程,建议使用 32 位版本的 Debian Buster 或 Ubuntu Xenial。它也可能在其他系统上工作,但为了避免不必要的复杂性,我建议使用推荐的系统之一设置虚拟机。如果您不熟悉虚拟机,请查看我的文章 在任何操作系统上使用 VirtualBox 试用 Linux。当您完成所有设置并运行后,还要安装 仓库描述 中提到的依赖项。还要考虑到您需要在虚拟机中访问互联网,并有足够的可用磁盘空间。我将我的虚拟机设置为 40GB 硬盘,这似乎足够了。
为了遵循本文中的说明,请克隆 pi-gen 仓库,或者如果您想开始开发自己的镜像,请 fork 它。
仓库概览
整个构建过程分为多个阶段。每个阶段都表示为一个普通文件夹,并代表关于完整 Raspberry Pi OS 镜像的逻辑中间步骤。
- 阶段 0:引导程序 — 创建一个可用的文件系统
- 阶段 1:最小系统 — 创建一个绝对最小的系统
- 阶段 2:精简系统 — 对应于 Raspberry Pi OS Lite
- 阶段 3:桌面系统 — 安装 X11、LXDE、Web 浏览器等等
- 阶段 4:对应于普通的 Raspberry Pi OS
- 阶段 5:对应于 Raspberry Pi OS Full
各个阶段是相互构建的:不构建较低阶段就无法构建较高阶段。您也不能遗漏中间的任何阶段。例如,要构建 Raspberry Pi OS Lite,您必须构建阶段 0、1 和 2。要构建带有桌面的 Raspberry Pi OS,您必须构建阶段 0、1、2、3、4 和 5。
构建过程
构建过程由 build.sh
控制,可以在根仓库中找到它。如果您已经知道如何读写 bash 脚本,那么理解那里定义的过程不会有障碍。如果不是,阅读 build.sh
并尝试理解正在发生的事情是一个非常好的实践。但是即使没有 bash 脚本编写技能,您也能够创建自己的预装 Cockpit 的镜像。
一般来说,构建过程由几个嵌套的 for 循环组成。
- stage-loop: 循环遍历所有阶段目录,按升序排列
- 如果找到名为 SKIP 的文件,则跳过进一步处理
- 运行脚本
prerun.sh
- sub-loop: 循环遍历每个子目录,按升序排列,并处理以下文件(如果存在)
-
00-run-sh
:预先运行的任意指令00-run-chroot.sh
:在镜像的 chroot 目录中运行此脚本00-debconfs
:debconf-set-selection
的变量00-packages
:要安装的软件包列表00-packages-nr
:类似于 00-packages,不同之处在于这将导致使用 --no-install-recommends -y 参数进行 apt-get 安装
00-patches
:一个包含要应用的补丁文件的目录,使用 quilt- 回到 stage-loop,如果找到名为
EXPORT_IMAGE
的文件,则为此阶段生成镜像 - 如果找到名为
SKIP_IMAGE
的文件,则跳过创建镜像
-
build.sh
还需一个名为 config
的文件,其中包含启动时读取的一些规范。
实践操作
首先,我们将创建一个基本的 Raspberry Pi OS Lite 镜像。Raspberry Pi OS Lite 镜像将作为我们自定义镜像的基础。创建一个名为 config 的空文件,并添加以下两行
IMG_NAME='Cockpit'
ENABLE_SSH=1
在目录 stage3
、stage4
和 stage5
中创建一个名为 SKIP
的空文件。阶段 4
和 5
默认会生成镜像,因此在 stage4
和 stage5
中添加一个名为 SKIP_IMAGE
的空文件。
现在打开终端,通过输入 su
切换到 root 用户。导航到仓库的根目录,并通过输入 ./build.sh
启动构建脚本。
构建过程将需要一些时间。
构建过程完成后,您将在仓库的根目录中找到另外两个目录:work
和 deploy
。work
文件夹包含一些中间输出。在 deploy
文件夹中,您应该找到压缩的镜像文件,已准备好部署。
如果整个构建过程成功,我们现在可以修改该过程,以便额外安装 Cockpit。
扩展构建过程
Raspberry Pi OS Lite 镜像充当我们安装 Cockpit 的基础。由于 Raspberry Pi OS Lite 镜像在 stage2
中已完成,我们将创建自己的 stage3
,它将处理 Cockpit 的安装。
我们完全删除原始的 stage3
,并创建一个新的、空的 stage3
rm -rf stage3 && mkdir stage3
在 stage3
中,我们创建一个用于安装 cockpit 的子阶段
mkdir stage3/00-cockpit
要在镜像上安装 cockpit,我们只需将其添加到软件包列表中
echo "cockpit" >> stage3/00-cockpit/00-packages
我们还希望配置新的 stage3
以输出镜像,因此我们只需在 stage3
目录中添加此文件
touch stage3/EXPORT_IMAGE
由于之前构建过程中已经存在中间镜像,我们可以通过在相关目录中添加 skip-files
来防止再次构建这些阶段
跳过 stage0
和 stage1
的构建过程
touch stage0/SKIP && touch stage1/SKIP
跳过 stage2
的构建过程,并跳过镜像创建
touch stage2/SKIP && touch stage2/SKIP_IMAGE
现在再次运行构建脚本
./build.sh
在 deployment
文件夹中,您现在应该找到一个压缩的镜像 <date>-Cockpit-lite.zip
,它已准备好部署。
故障排除
如果您尝试应用更复杂的修改,那么使用 pi-gen 构建自己的 Raspberry Pi 镜像会涉及大量的尝试和错误。您肯定会遇到构建过程由于某种原因在中间停止的情况。由于构建过程中没有异常处理,因此如果过程停止,我们需要手动进行一些清理。
在过程停止后,chroot
文件系统很可能仍处于挂载状态。如果不卸载它,您将无法启动新的构建过程。如果它仍然挂载,请通过输入以下命令手动卸载它
umount work/<Build-date-&-image-name>/tmpimage/
我确定的另一个问题是,当 chroot
文件系统即将被卸载时,脚本停止了。在文件 scripts/qcow2_handling
中,您可以看到在尝试卸载之前直接调用了 sync
。Sync
强制系统刷新写入缓冲区。当以虚拟机方式运行构建系统时,当调用 unmount
时,写入过程尚未准备就绪,因此脚本在此处停止。
为了解决这个问题,我只是在 sync
和 unmount
之间插入了一个 sleep
,这解决了这个问题

(我知道 30 秒有点过分了,但由于整个构建过程需要 20 多分钟,30 秒只是沧海一粟)
修改现有镜像
与使用 pi-gen
构建镜像相反,您也可以直接在正在运行的 Raspberry Pi OS 上应用修改。在我们的场景中,只需登录并使用以下命令安装 Cockpit
sudo apt install cockpit
现在关闭您的 Raspberry Pi,取出 SD 卡,并将其连接到您的 PC。通过输入 lsblk -p
检查您的系统是否已自动挂载 SD 卡上的分区

在上面的屏幕截图中,SD 卡是设备 /dev/sdc
,并且 boot
和 rootfs
分区已自动挂载到提到的挂载点。在继续之前,请使用以下命令卸载它们
umount /dev/sdc1 && umount /dev/sdc2
现在我们将 SD 卡的内容复制到我们的文件系统中。确保您有足够的可用磁盘空间,因为镜像的大小将与 SD 卡相同。使用以下命令启动复制过程
dd if=/dev/sdc of=~/MyImage.img bs=32M

复制过程完成后,我们可以使用 PiShrink 缩小镜像。按照仓库中提到的安装说明进行操作,即
wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh
chmod +x pishrink.sh
sudo mv pishrink.sh /usr/local/bin
现在通过输入以下内容调用脚本
sudo pishrink.sh ~/MyImage.img

PiShrink 将镜像大小缩小了近十倍:从之前的 30GB 缩小到 3.5GB。您仍然可以通过在上传或共享之前对其进行压缩来优化大小。
就是这样,您现在可以共享和刷写此镜像了。
刷写镜像
如果您想使用 Linux 将您自己的自定义 Raspberry Pi 镜像刷写回 SD 卡,请按照以下步骤操作。
将 SD 卡插入您的 PC。如果之前已经安装过系统,您的系统很可能会自动挂载 SD 卡上的文件系统。您可以通过打开命令行并输入 lsblk -p
来检查这一点

正如您在上面的屏幕截图中看到的,我的系统自动挂载了两个文件系统 boot
和 rootfs
,因为此 SD 卡已包含 Raspberry Pi OS。在我们开始刷写 SD 卡之前,我们必须首先卸载文件系统,方法是输入
umount /dev/sdc1 && umount /dev/sdc2
lsblk -p
的输出应如下所示才能继续

现在您可以将镜像刷写到 SD 卡:打开命令行并输入
dd if=/path/to/image.img of=/dev/sdc bs=32M, conv=fsync
使用 bs=32M
,您指定 SD 卡以 32 兆字节的块写入,conv=fsync
强制该过程物理写入每个块。
如果成功,您应该看到此输出

完成!您现在可以将 SD 卡放回 Raspberry Pi 并启动它。
总结
本文介绍的两种技术都有其优点和缺点。虽然使用 pi-gen
创建自己的自定义 Raspberry Pi 镜像比简单地修改现有镜像更容易出错,但如果您计划设置 CICD 管道,这是首选方法。我个人最喜欢的显然是修改现有镜像,因为您可以直接确保您应用的更改正在工作。
评论已关闭。