Skip to content

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 into a single-layer overlayfs (following the Docker principle). The final output of buildImage is a tar.gz that can be used by docker import. See rootfs-tar.nix.

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 $out/root
        cp -r $src/* $out/
      '';

  # 使用 buildImage 创建 Docker 镜像(单层)
  # 直接返回 dockerImage,解压逻辑在 default.nix 中处理
  dockerImage = pkgs.dockerTools.buildImage {
    name = "busybox-rootfs";
    copyToRoot = [
      sys-config
    ]
    ++ apps;
    keepContentsDirlinks = false;
  };

in
dockerImage

Unpack the Docker tar and build the filesystem image

The idea: unpack the Docker image, take layer.tar from the first layer, then import it into a virtual disk image with guestfish. See user/default.nix.

shell
{
  lib,
  pkgs,
  nixpkgs,
  system,
  target,
  fenix,
  buildDir,
  testOpt,
  rootfsType ? "vfat",
  diskPath,
  partitionType ? "mbr",
}:

let
  image = import ./rootfs-tar.nix {
    inherit
      lib
      pkgs
      nixpkgs
      system
      target
      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
    }
    trap 'cleanup_loop; chmod +w -R "$TEMP_DIR" 2>/dev/null && rm -rf "$TEMP_DIR"; [ -n "$EXTRACT_DIR" ] && chmod +w -R "$EXTRACT_DIR" 2>/dev/null && rm -rf "$EXTRACT_DIR"' EXIT

    # 等待分区设备出现
    echo "    Waiting for partition device..."
    for _ in $(seq 1 10); do
      if [ -b "''${LOOP_DEV}p1" ]; then
        break
      fi
      sleep 0.1
    done

    if [ ! -b "''${LOOP_DEV}p1" ]; then
      echo "Error: Partition device ''${LOOP_DEV}p1 not found"
      exit 1
    fi

    # 格式化分区
    echo "    Formatting partition as ${rootfsType}..."
    ${mkfsCommand}

    # 挂载分区
    MOUNT_DIR=$(mktemp -d)
    echo "    Mounting partition to $MOUNT_DIR..."
    sudo mount "''${LOOP_DEV}p1" "$MOUNT_DIR"

    # 更新 trap 以包含 MOUNT_DIR
    # shellcheck disable=SC2317,SC2329
    cleanup_loop() {
      echo "    Cleaning up..."
      sudo umount "$MOUNT_DIR" 2>/dev/null || true
      sudo losetup -d "$LOOP_DEV" 2>/dev/null || true
      rm -rf "$MOUNT_DIR" 2>/dev/null || true
    }
    trap 'cleanup_loop; chmod +w -R "$TEMP_DIR" 2>/dev/null && rm -rf "$TEMP_DIR"; [ -n "$EXTRACT_DIR" ] && chmod +w -R "$EXTRACT_DIR" 2>/dev/null && rm -rf "$EXTRACT_DIR"' EXIT

    # 解压 tar 到分区
    echo "    Extracting rootfs to partition..."
    sudo tar -xf "$FINAL_TAR" -C "$MOUNT_DIR"
    sudo chmod 0755 "$MOUNT_DIR"

    # 同步并卸载
    echo "    Syncing and unmounting..."
    sync
    sudo umount "$MOUNT_DIR"
    sudo losetup -d "$LOOP_DEV"
    rm -rf "$MOUNT_DIR"

    # 重置 trap
    trap 'chmod +w -R "$TEMP_DIR" 2>/dev/null && rm -rf "$TEMP_DIR"; [ -n "$EXTRACT_DIR" ] && chmod +w -R "$EXTRACT_DIR" 2>/dev/null && rm -rf "$EXTRACT_DIR"' EXIT
  '';

  # 构建脚本 - 在bin/目录下构建
  buildScript = pkgs.writeShellApplication {
    name = "dragonos-rootfs";
    runtimeInputs = [
      pkgs.coreutils
      pkgs.gnutar
      pkgs.libguestfs-with-appliance
      pkgs.findutils
      pkgs.parted
      pkgs.dosfstools
      pkgs.e2fsprogs
      pkgs.util-linux
    ];
    text = ''
      set -euo pipefail

      # Ensure build directory exists
      mkdir -p "${buildDir}"

      OUTPUT_TAR="${buildDir}/rootfs.tar"

      echo "==> Generating rootfs"

      # 创建临时目录
      TEMP_DIR=$(mktemp -d)
      EXTRACT_DIR=""  # 初始化为空,vfat 处理时会赋值
      trap 'chmod +w -R "$TEMP_DIR" 2>/dev/null && rm -rf "$TEMP_DIR"; [ -n "$EXTRACT_DIR" ] && chmod +w -R "$EXTRACT_DIR" 2>/dev/null && rm -rf "$EXTRACT_DIR"' EXIT

      # 提取 layer.tar (rootfs)
      echo "  Extracting rootfs layer..."
      cd "$TEMP_DIR"
      tar -xzf ${image}

      # 找到 layer.tar 并复制到 bin/
      LAYER_TAR=$(find . -name "layer.tar" | head -1)
      if [ -z "$LAYER_TAR" ]; then
        echo "Error: layer.tar not found in docker image"
        exit 1
      fi

      cp "$LAYER_TAR" "$OLDPWD/$OUTPUT_TAR"
      cd "$OLDPWD"
      chmod +w "$OUTPUT_TAR"

      TAR_SIZE=$(du -h "$OUTPUT_TAR" | cut -f1)
      echo "  ✓ rootfs.tar created ($TAR_SIZE)"

      # 根据文件系统类型处理 rootfs(Nix 编译时决定)
      ${rootfsProcessing}

      echo "==> Building disk image at ${diskPath}"

      # 创建磁盘镜像并初始化文件系统
      echo "  Creating disk image..."
      TEMP_IMG="${diskPath}.tmp"

      # 计算所需磁盘大小:tar包大小 + 1G 缓冲空间
      TAR_SIZE_KB=$(du -k "$FINAL_TAR" | cut -f1)
      DISK_SIZE_KB=$(( TAR_SIZE_KB + 1024 * 1024 ))
      truncate -s "''${DISK_SIZE_KB}K" "$TEMP_IMG"

      # 检查是否使用非特权构建模式(guestfish)
      if [ "''${DRAGONOS_UNPRIVILEGED_BUILD:-0}" = "1" ]; then
        ${guestfishWrite}
      else
        ${loopWrite}
      fi

      mv -f "$TEMP_IMG" "${diskPath}"

      IMG_SIZE=$(du -h "${diskPath}" | cut -f1)
      echo "  ✓ disk image created ($IMG_SIZE)"

      echo "==> Build complete!"
      echo "    Rootfs tar: $OUTPUT_TAR"
      echo "    Disk image: ${diskPath}"
    '';
  };

in
buildScript

Behind the Nix Script

When generating a Docker image, the Docker Image is cached as a binary artifact in /nix/store, so multiple builds may quickly consume disk space.

hint: To free up disk space, run nix store gc

However, the second step—extracting the Docker tar to a filesystem disk image file—is implemented by generating and running a shell script. The generated disk-image-${target}.img will only exist in the bin directory. Meanwhile, the rootfs.tar extracted from .tar.gz will also reside in the bin directory. You can inspect it yourself to see what's inside.

TODO: Future scripts will allow a two-step process, with the option to inject custom files in between (in addition to direct injection via sysconfig).

If you want to see what this script looks like, you can run nix build .#rootfs-${target} -o bin/result in the project root directory. Nix will link the script's corresponding derivation to the result directory. Simply cat bin/result/bin/build-rootfs-image to view all the commands.