从软件包到RootFS镜像
定义完软件包后,无需做更多修改,直接 nix run .#rootfs-${target} 即可构建 RootFS。
生成 Docker 镜像
我们利用 pkgs.dockerTools.buildImage 的展平和拷贝属性,将 user/apps 下的软件包,以及 user/sysconfig
中的配置文件复制到单层 overlayfs 中(Docker原理),buildImage 的终产物是一个可以被 docker import 的一个
tar.gz。见 rootfs-tar.nix
{ lib, pkgs, nixpkgs, system, target, fenix, testOpt }:
# 产物是一个可以生成 rootfs.tar 的脚本
let
apps = import ./apps { inherit lib pkgs nixpkgs system target fenix testOpt; };
sys-config = pkgs.runCommand "sysconfig" {
src = ./sysconfig;
} ''
mkdir -p $out
cp -r $src/* $out/
'';
# 使用 buildImage 创建 Docker 镜像(单层)
# 直接返回 dockerImage,解压逻辑在 default.nix 中处理
dockerImage = pkgs.dockerTools.buildImage {
name = "busybox-rootfs";
copyToRoot = [
sys-config
] ++ apps;
keepContentsDirlinks = false;
};
in dockerImage
解压 Docker tar 并生成文件系统镜像
原理:解压 docker 镜像,拿出第一层中的 layer.tar,然后使用 guestfish 导入到虚拟镜像文件中。见 user/default.nix
fenix
testOpt
;
};
# parted 使用 msdos 而不是 mbr
partedLabel = if partitionType == "mbr" then "msdos" else partitionType;
# 根据文件系统类型生成 mkfs 命令
mkfsCommand =
if rootfsType == "vfat" then
''sudo mkfs.vfat "''${LOOP_DEV}p1"''
else if rootfsType == "ext4" then
''sudo mkfs.ext4 -F "''${LOOP_DEV}p1"''
else
''sudo mkfs -t ${rootfsType} "''${LOOP_DEV}p1"'';
# vfat 特殊处理脚本片段
vfatProcessing = ''
echo " Processing rootfs for vfat (excluding /nix/store, dereferencing symlinks)..."
EXTRACT_DIR=$(mktemp -d)
FILTERED_TAR="${buildDir}/rootfs-filtered.tar"
# 解压原始 tar,排除 /nix/store
echo " Extracting and not filtering..."
chmod +w -R "$TEMP_DIR" "$EXTRACT_DIR"
fakeroot tar --owner=0 --group=0 --numeric-owner --exclude='proc' --exclude='dev' \
--exclude='sys' -xf "$OUTPUT_TAR" -C "$EXTRACT_DIR"
# 重新打包,解引用符号链接和硬链接
echo " Re-packing with dereferenced links..."
fakeroot tar --owner=0 --group=0 --numeric-owner --dereference --hard-dereference -cf "$FILTERED_TAR" -C "$EXTRACT_DIR" .
FILTERED_SIZE=$(du -h "$FILTERED_TAR" | cut -f1)
echo " ✓ Re-packed rootfs.tar created ($FILTERED_SIZE)"
FINAL_TAR="$FILTERED_TAR"
'';
# 非 vfat 不需要特殊处理
nonVfatProcessing = ''
FINAL_TAR="$OUTPUT_TAR"
'';
# 根据 rootfsType 选择处理逻辑
rootfsProcessing = if rootfsType == "vfat" then vfatProcessing else nonVfatProcessing;
# guestfish 写盘逻辑
guestfishWrite = ''
echo " Using guestfish (unprivileged mode)..."
export LIBGUESTFS_CACHEDIR=/tmp
export LIBGUESTFS_BACKEND=direct
# 使用 guestfish 创建分区并注入 tar
echo " Initializing disk and copying rootfs..."
guestfish -a "$TEMP_IMG" <<EOF
run
part-init /dev/sda ${partitionType}
part-add /dev/sda primary 2048 -2048
mkfs ${rootfsType} /dev/sda1
mount /dev/sda1 /
tar-in $FINAL_TAR /
chmod 0755 /
umount /
sync
shutdown
EOF
'';
# loop 设备写盘逻辑
loopWrite = ''
echo " Using loop device (privileged mode, faster)..."
# 使用 parted 创建分区表和分区
echo " Creating partition table..."
parted -s "$TEMP_IMG" mklabel ${partedLabel}
parted -s "$TEMP_IMG" mkpart primary ${rootfsType} 1MiB 100%
# 设置 loop 设备
echo " Setting up loop device..."
LOOP_DEV=$(sudo losetup --find --show --partscan "$TEMP_IMG")
echo " Loop device: $LOOP_DEV"
# 确保清理 loop 设备
# shellcheck disable=SC2317,SC2329
cleanup_loop() {
echo " Cleaning up loop device..."
sudo umount "''${LOOP_DEV}p1" 2>/dev/null || true
sudo losetup -d "$LOOP_DEV" 2>/dev/null || true
nix script 的背后
生成 Docker 镜像时,Docker Image 是作为二进制产物缓存在 /nix/store 中的,因此多次构建可能会快速占用硬盘。
hint: 要想释放磁盘空间,执行 nix store gc
但第二步:解压 Docker tar 到文件系统磁盘镜像文件,是以生成一个 shell script 并运行来实现的,生成的 disk-image-${target}.img 只会存在一个在 bin 目录下;同时从 .tar.gz 中解压出来的 rootfs.tar 也会存在于 bin 目录下,你可以自己解压看看里面都存了些啥。
TODO: 后续脚本允许分两步进行,中间允许注入自定义文件(除了直接在 sysconfig 中注入外)
如果你想看看这个脚本长什么样,你可以在项目根目录执行 nix build .#rootfs-${target} -o bin/result 。 nix 会把 script 对应的 derivation 链接到 result 目录下,只需 cat bin/result/bin/build-rootfs-image 就能看到全部命令。