小类随手记

用 NixOS 构建可直接启动的 raw 磁盘镜像

记录用 Flakes 和 image.repart 构建 GPT raw 镜像的实现思路,可以实现在物理机和虚拟机启动的虚拟磁盘文件。

目的

  • 避免新安装的麻烦:在新机器上只要有efi/esp分区和一个大分区,只需要复制一个efi文件和一个虚拟磁盘文件即可完成安装。甚至在github上可以直接下载本文的构建结果。
  • 可以物理机和虚拟机启动同一个系统:在win/macos下可以用虚拟机启动nixos,也可以物理机直接启动。
  • 比ventoy更可靠,完全使用nix和grub的机制

同一个 nixos.raw 有三种用法:

  • 用 dd 写到 U 盘或硬盘,开机从它启动;
  • 放在任意 NTFS 分区的 OS-img/nixos.raw,由另外一个定制的 EFI 文件找到并完成后续启动国产;
  • 直接作为虚拟磁盘,交给 VMware、VirtualBox、Hyper-V 或 QEMU。

三种方式启动后是同一个系统,没有任何区别。

两个配置的分工

仓库里和 Raw OS 相关的配置有两类,职责不同。

raw_os_core_img_creat(machine/raw_os/raw-os-core-img-creat.nix)只负责生成基础镜像(体积为10G 压缩后后1.6G左右)。系统里只有常用的命令行工具、NetworkManager、fish 和 nixos 用户(用户名密码都是 nixos,root用户的密码也是nixos,),足够第一次启动后做后续配置。为了首次使用方便,基础镜像默认自动登录。

raw_os_work_amd 和 raw_os_work_intel是运行在镜像里的日常环境。区别是 amd 或 intel tag 决定 IOMMU、KVM、VFIO 的配置以及部分驱动等,同时加载桌面、开发、输入法等模块。

也就是 raw_os_core_img_creat 用来创建 nixos.raw这个文件,方便部署。然后启动系统在这个基础系统内 运行sudo nixos-rebulid switch --flake .#raw_os_work_amd/intel 来完成日用环境

镜像的结构

镜像内部分区

  • 分区 1 esp:vfat,固定 1 GiB,分区标签 nixos-esp。
  • 分区 2 root:btrfs,固定 10 GiB,分区标签 nixos-root,挂载参数带 compress=zstd。

ESP 里放四个文件:

1
2
3
4
EFI/BOOT/BOOTX64.EFI                               # 主引导
EFI/NixOS/raw-os/grub.cfg
EFI/NixOS/raw-os/nixos-current.efi                 # nixos generation 当前
EFI/NixOS/raw-os/nixos-generation-1.efi            # nixos generation 其他历史(限于容量 最多储存8个,在初始的nxios.raw中只有一个)

构建镜像

1
nix build --no-link --print-out-paths .#nixosConfigurations.raw_os_core_img_creat.config.system.build.image

命令输出一个 /nix/store/...-nixos-disk-image 目录,里面的 nixos.raw 就是最终镜像。把它复制到目标位置,例如 NTFS 分区:

1
cp --reflink=auto /nix/store/...-nixos-disk-image/nixos.raw /mnt/d/OS-img/nixos.raw

写盘启动也是同一个文件:

1
sudo dd if=./nixos.raw of=/dev/sdX bs=16M status=progress conv=fsync

外部 EFI

想在不改分区表的前提下启动这个镜像,需要第二个 EFI 文件 rawOsExternalBootEfi。它同样是 standalone GRUB,但内嵌的配置不一样:它会在 NTFS 分区上搜索 /OS-img/nixos.raw,用 loopback 把镜像映射成一块磁盘,然后加载 (raw,gpt1)/EFI/NixOS/raw-os/grub.cfg。同时会搜索是否有Windows的引导,然后构建一个启动菜单里面包括:nixos.raw内的多个generation 以及自动搜索启动Windows

构建在各个efi的 方法是执行脚本:(具体参考仓库:https://github.com/joyanhui/myos)

1
machine/raw_os/docs_script/creat-rawOsExternalBootEfi.sh -o ./nixos-img.efi

外部 EFI 只需要部署一次,它不保存 nixos-current.efi、grub.cfg 和 nixos-generation-*.efi,这些文件都在镜像内部。只有改了外部 EFI 的搜索路径、启动逻辑、内嵌模块,或者更新 nixpkgs 后需要重新验证、文件损坏时,才需要重新构建并手动替换。

从 NTFS 启动时根分区从哪里来

外部 EFI 把镜像当作一块磁盘来读,此时 NixOS 内核已经启动,但镜像里的分区还没有出现在 /dev 下,因为镜像内的 GPT 分区要先由 loop 设备导出。这部分由 initrd 完成。

raw-os-runtime.nix 在 initrd 里注册了 nixos-img-loop-for-root.service,Type=oneshot,wantedBy = [ "initrd.target" ],并且排在 initrd-root-device.target 和 sysroot.mount 之前。脚本的逻辑是:

  1. 等 10 秒,直到出现 /dev/vd*、/dev/sd*、/dev/nvme*n*p* 这类块设备;
  2. 检查是否有分区的标签已经是 nixos-root,有就直接退出——这是写盘启动和虚拟机启动的情况;
  3. 否则逐个尝试用 ntfs3 挂载宿主的分区,找到 OS-img/nixos.raw 的所在的分区;
  4. 对镜像执行 losetup -fP,再 udevadm trigger 和 udevadm settle,等 loop 分区出现;
  5. 按标签给每个 loop 分区建立 /dev/disk/by-partlabel/<label> 链接。

在运行中的系统里更新

在已经启动的 Raw OS 内,进入仓库目录执行:

1
sudo nixos-rebuild switch --flake .#raw_os_work_amd

Intel 机器把目标换成 raw_os_work_intel。

raw-os-runtime.nix 通过 boot.loader.external.installHook 挂上了 installRawBoot 脚本,nixos-rebuild switch 在最后阶段执行它。它做的事情是:

  1. 读取 $toplevel/boot.json,取出 kernel、initrd、init、initrdSecrets 和 kernelParams;
  2. 把 initrd 复制到临时目录,如果配置了 initrd secrets 就追加进去,避免直接修改 store 里的文件;
  3. 调用 ukify build,用 linux<arch>.efi.stub 作 stub,cmdline 拼成 init=<init> <kernelParams>,生成 UKI;
  4. 扫描 /nix/var/nix/profiles/system-*-link,按 generation 编号倒序排列,并用 readlink -f 的结果匹配当前 toplevel,得到当前 generation 的编号;
  5. 选出当前 generation 加上至多 8 个 generation,为每个生成 nixos-generation-<n>.efi;
  6. 生成新的 grub.cfg:第一项是 nixos-current.efi,之后是各 generation 的倒序菜单,最后是 Windows Boot Manager;
  7. 先删除不在列表里的旧 generation 文件,然后依次安装 current、各 generation、grub.cfg,再清理一次,最后安装 EFI/BOOT/BOOTX64.EFI;
  8. 所有文件都是先写临时文件再 mv -f 替换,中途出错不会留下半个 UKI。

nixos-current.efi 单独保留,不占 generation 的数量限制。boot.loader.efi.canTouchEfiVariables = false,整个过程不会修改 UEFI 的启动变量。

ESP 只有 1 GiB,所以脚本选择先清理旧 generation 再安装新文件,这样不需要额外空间来暂存新的 UKI。代价是如果安装过程失败,被删掉的旧 generation 已经找不回来,重启后菜单里能回滚的项会变少。

在虚拟机里启动

nixos.raw 是标准格式的虚拟磁盘,虚拟机软件基本都能直接用。

  • VMware Workstation 和 ESXi 可以直接把 raw 文件当作虚拟磁盘,也可以创建一个 VMDK 映射它,客户机固件设置为 UEFI。VMware 的虚拟显卡性能在这几个方案里最好,关闭动态壁纸和毛玻璃,装好客户机工具之后,niri 的体验接近物理机。
  • VirtualBox 原生支持 raw 磁盘,只需要把系统设置成 UEFI 启动。
  • Hyper-V 要先用 Arsenal-Image-Mounter 之类的工具把镜像挂载成物理磁盘,在磁盘管理里把它设为脱机,然后在第二代虚拟机的磁盘设置里选择这块物理硬盘。仓库里还有一个 additional-foot-to-raw-and-renameVHD.sh,会给 raw 文件加上 VHD 尾部并改名为 .vhd,第一代虚拟机可以直接使用。
  • QEMU 直接用 -drive format=raw,详见下面的测试命令。

在 Windows 下用 QEMU 启动的完整命令:

1
2
3
4
5
6
7
8
9
& "C:\Program Files\qemu\qemu-system-x86_64.exe" `
  -machine q35,accel=whpx -cpu max -m 8G -smp 4 `
  -smbios type=11,value=io.systemd.stub.kernel-cmdline-extra=nomodeset `
  -bios N:\OS-img\OVMF.fd `
  -drive "if=virtio,format=raw,file=N:\OS-img\nixos.raw,cache=writeback" `
  -vga std -display sdl `
  -device qemu-xhci -device usb-tablet `
  -netdev user,id=net0 -device virtio-net-pci,netdev=net0 `
  -serial "file:N:\OS-img\qemu-serial.log"

io.systemd.stub.kernel-cmdline-extra 是给 UKI 追加内核参数,这里加了 nomodeset,避免在虚拟显卡上因为没有对应驱动而黑屏。如果启动后停在 EFI Shell,输入 FS0:\EFI\BOOT\BOOTX64.EFI 手动启动。OVMF 固件可以从 QEMU 安装目录获取,仓库的 kvm-config/ 下也有副本,用法见 KVM 虚拟机使用 OVMF 模式 UEFI 启动。

扩容

根分区固定 10 GiB,装上日常环境后空间会比较紧张。仓库里有两个等价的实现:镜像自带的 resize-raw-image(和本仓库的 docs_script/resize-raw-image.sh,已经加入系统包),以及 Windows 下的 resize-raw-image.ps1。

相关文章

comments powered by Disqus
Theme Stack