From Software Packages to RootFS Image
After defining the software packages, no further modifications are needed. Simply nix run .#rootfs-${target} to build the RootFS.
Generating a Docker Image
We leverage the flattening and copying properties of pkgs.dockerTools.buildImage to copy the packages under user/apps and the configuration files in user/sysconfig (together with their /nix/store closure) into a single-layer image (following the Docker principle). The output of buildImage is an uncompressed tar that can be used by docker load. See rootfs-tar.nix.
{
lib,
pkgs,
nixpkgs,
system,
target,
testOpt,
nightlyRustPlatform,
}:
# 产物是一个未压缩的 docker 镜像 tar,其中的 layer.tar 即 rootfs 内容
let
apps = import ./apps {
inherit
lib
pkgs
nixpkgs
system
target
testOpt
nightlyRustPlatform
;
};
sys-config =
pkgs.runCommand "sysconfig"
{
src = ./sysconfig;
}
''
mkdir -p $out $out/root
cp -r $src/* $out/
'';
# 使用 buildImage 创建 Docker 镜像(单层),copyToRoot 会连同 /nix/store 闭包一起打包
dockerImage = pkgs.dockerTools.buildImage {
name = "busybox-rootfs";
copyToRoot = [
sys-config
]
++ apps;
keepContentsDirlinks = false;
# Create writable runtime directories in the layer, after Nix store
# permission normalization. /var/tmp must remain disk-backed for tests.
extraCommands = ''
mkdir -p var/tmp
chmod 1777 var/tmp
'';
# 仅作为 disk-image.nix 的中间产物,不需要压缩
compressor = "none";
};
in
dockerImageGenerating the Disk Image
Inside the Nix sandbox, the first layer's layer.tar is extracted under fakeroot, then mke2fs -d writes the directory straight into a partition at a 1MiB offset in the disk image, and sfdisk writes the MBR partition table (the kernel mounts the rootfs from /dev/vda1). No root privileges or loop devices are needed. See user/disk-image.nix.
{
lib,
pkgs,
rootfsImage,
rootfsType ? "ext4",
partitionType ? "mbr",
}:
# 在 nix 沙箱内免特权生成带 MBR 分区表的磁盘镜像:
# 单个分区从 1MiB 开始,内核从 /dev/vda1 挂载 rootfs。
# 产物会被 nix 缓存,userland 不变时无需重新生成。
assert lib.assertMsg (
partitionType == "mbr"
) "disk-image.nix: only mbr partition table is supported";
assert lib.assertMsg (lib.elem rootfsType [
"ext4"
"vfat"
]) "disk-image.nix: unsupported rootfsType ${rootfsType}";
let
partOffsetSectors = 2048;
partOffsetBytes = partOffsetSectors * 512;
# 各文件系统的差异:内容大小统计方式、MBR 分区类型、写入命令
# ($ROOT 为 rootfs 目录,$PART_KB 为分区大小)
fs =
if rootfsType == "ext4" then
{
prepare = "";
# 写入时需要 fakeroot 让文件属主为 root
wrap = "fakeroot -i fakeroot.state --";
contentKb = ''$(( $(stat -c %s "$LAYER_TAR") / 1024 ))'';
partType = "83";
mkfs = ''
mke2fs -q -t ext4 -F -d "$ROOT" -E offset=${toString partOffsetBytes} disk.img "''${PART_KB}k"
'';
}
else
{
# vfat 不支持符号链接,mcopy 会解引用复制。先把绝对链接改写到 $ROOT 下,
# 避免解引用到构建机自身的 /nix/store;镜像大小也按解引用后的大小计算
prepare = ''
chmod -R u+w "$ROOT"
find "$ROOT" -type l -lname '/*' | while read -r link; do
ln -sfn "$ROOT$(readlink "$link")" "$link"
done
'';
contentKb = ''$(du -sklL "$ROOT" | cut -f1)'';
# vfat 没有属主概念,不需要 fakeroot
wrap = "";
partType = "c";
mkfs = ''
mkfs.vfat --offset ${toString partOffsetSectors} disk.img "$PART_KB"
mcopy -s -i disk.img@@${toString partOffsetBytes} "$ROOT"/* ::/
'';
};
in
pkgs.runCommand "dragonos-disk-image"
{
nativeBuildInputs = with pkgs; [
gnutar
fakeroot
e2fsprogs
util-linux
dosfstools
mtools
];
}
''
LAYER_TAR=$(tar -tf ${rootfsImage} | grep '/layer.tar$' | head -1)
if [ -z "$LAYER_TAR" ]; then
echo "Error: layer.tar not found in ${rootfsImage}"
exit 1
fi
tar -xf ${rootfsImage} "$LAYER_TAR"
# fakeroot 保证解包与写入文件系统时属主均为 root(-s/-i 在两次调用间保存属主信息)
ROOT=$PWD/root
mkdir "$ROOT"
fakeroot -s fakeroot.state -- tar -xf "$LAYER_TAR" -C "$ROOT"
${fs.prepare}
# 磁盘大小:rootfs 大小 + 1GiB 余量
DISK_KB=$(( ${fs.contentKb} + 1024 * 1024 ))
PART_KB=$(( DISK_KB - ${toString partOffsetBytes} / 1024 ))
truncate -s "''${DISK_KB}K" disk.img
echo 'start=${toString partOffsetSectors}, type=${fs.partType}' | sfdisk -q --label dos disk.img
export ROOT PART_KB
${fs.wrap} sh -euc '
chmod 0755 "$ROOT"
${fs.mkfs}
'
mkdir -p $out
mv disk.img $out/disk.img
''nix build .#checks.x86_64-linux.rootfs-image-x86_64 validates the generated image (partition table, e2fsck, ownership of key files); see user/check-disk-image.sh.
Behind the Nix Script
The disk image is an ordinary Nix derivation cached in /nix/store: it is not regenerated unless user/ changes. Old copies remain after repeated changes to user/ and may take up a lot of disk space.
hint: To free up disk space, run nix store gc
For interactive runs, the script run by nix run .#rootfs-${target} (user/default.nix) only copies the image from the store to the writable bin/disk-image-${target}.img when it has changed, recording its source in bin/.disk-image-${target}.src. When the image is unchanged the copy is skipped, so changes made to the disk inside DragonOS are kept; -- --force restores a clean image. When image preparation selects AUTO_TEST=dunit or AUTO_TEST=syscall (including make test-dunit and make test-syscall), it always restores the clean image and discards guest disk changes.
{
lib,
pkgs,
nixpkgs,
system,
target,
buildDir,
testOpt,
nightlyRustPlatform,
rootfsType ? "vfat",
diskPath,
partitionType ? "mbr",
}:
let
rootfsImage = import ./rootfs-tar.nix {
inherit
lib
pkgs
nixpkgs
system
target
testOpt
nightlyRustPlatform
;
};
diskImage = import ./disk-image.nix {
inherit
lib
pkgs
rootfsImage
rootfsType
partitionType
;
};
# 磁盘镜像在 nix store 中构建并缓存;这里只负责在其变化时拷贝出可写副本
buildScript = pkgs.writeShellApplication {
name = "dragonos-rootfs";
runtimeInputs = [
pkgs.coreutils
pkgs.util-linux
];
text = ''
SRC_IMG="${diskImage}/disk.img"
DST_IMG="${diskPath}"
STAMP="${buildDir}/.disk-image-${target}.src"
FORCE=0
if [ "''${1:-}" = "-f" ] || [ "''${1:-}" = "--force" ]; then
FORCE=1
fi
# Test runs require a fresh immutable baseline; interactive runs retain
# guest changes. Match the runner's runtime/configured AUTO_TEST choice.
case "''${AUTO_TEST:-${testOpt.autotest}}" in
dunit|syscall) FORCE=1 ;;
esac
mkdir -p "${buildDir}" "$(dirname "$DST_IMG")"
# Keep the lock file: unlinking it would allow callers to lock different
# inodes. The image and its source stamp form a single transaction.
exec {lock_fd}>"$DST_IMG.lock"
flock -x "$lock_fd"
if [ "$FORCE" = "0" ] && [ -f "$DST_IMG" ] && [ "$(cat "$STAMP" 2>/dev/null)" = "$SRC_IMG" ]; then
echo "==> Disk image unchanged, skip: $DST_IMG"
echo " (use 'nix run .#rootfs-${target} -- --force' to discard changes made inside the guest)"
exit 0
fi
echo "==> Copying disk image to $DST_IMG"
TEMP_IMG=""
TEMP_STAMP=""
trap 'rm -f -- "$TEMP_IMG" "$TEMP_STAMP"' EXIT
TEMP_IMG=$(mktemp "$DST_IMG.tmp.XXXXXX")
TEMP_STAMP=$(mktemp "$STAMP.tmp.XXXXXX")
cp --sparse=always --reflink=auto "$SRC_IMG" "$TEMP_IMG"
chmod u+w "$TEMP_IMG"
printf '%s\n' "$SRC_IMG" > "$TEMP_STAMP"
# Invalidate the old source before replacing its image. A crash between
# the two publications must force a rebuild, including for the old source.
rm -f -- "$STAMP"
mv -f -- "$TEMP_IMG" "$DST_IMG"
# Publish the stamp last, so a failed copy/publication is retried.
mv -f -- "$TEMP_STAMP" "$STAMP"
echo "==> Build complete!"
echo " Disk image: $DST_IMG ($(du -h "$DST_IMG" | cut -f1))"
'';
passthru = { inherit diskImage; };
};
in
buildScriptTo inspect the final image, run nix build .#packages.x86_64-linux.rootfs-x86_64.diskImage -o bin/result in the project root; the image is at bin/result/disk.img.