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 (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.

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
dockerImage

Generating 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.

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.

nix
{
  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
buildScript

To 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.