diff --git a/flake.nix b/flake.nix index 6123421..8b7c736 100644 --- a/flake.nix +++ b/flake.nix @@ -26,6 +26,16 @@ config.allowUnfree = true; }); in { + nixosConfigurations = { + installer = nixpkgs.lib.nixosSystem { + modules = [ + "${nixpkgs}/nixos/modules/installer/cd-dvd/channel.nix" + ./machines/installer + ]; + specialArgs = {inherit inputs;}; + }; + }; + packages = forAllSystems (system: let pkgs = nixpkgsFor.${system}; in { @@ -42,6 +52,23 @@ .system .build .vm; + + installer-demo = pkgs.writeShellScript "installer-demo" '' + set -euo pipefail + disk=installer-demo-root.img + ${pkgs.qemu}/bin/qemu-img create -f qcow2 "$disk" 80G + ${pkgs.qemu}/bin/qemu-system-x86_64 \ + -cpu host \ + -enable-kvm \ + -m 8G \ + -vga virtio \ + -display gtk,full-screen=on,grab-on-hover=on \ + -bios ${pkgs.OVMF.fd}/FV/OVMF.fd \ + -cdrom ${self.packages.${system}.installer-iso}/iso/*.iso \ + -hda "$disk" + ''; + + installer-iso = inputs.self.nixosConfigurations.installer.config.system.build.isoImage; }); apps = forAllSystems (system: { @@ -58,6 +85,11 @@ type = "app"; program = "${clean-state-demo}/bin/run"; }; + + installer-demo = { + type = "app"; + program = "${self.packages.${system}.installer-demo}"; + }; }); formatter = forAllSystems (system: nixpkgsFor.${system}.alejandra); diff --git a/machines/installer/default.nix b/machines/installer/default.nix new file mode 100644 index 0000000..13dd2bd --- /dev/null +++ b/machines/installer/default.nix @@ -0,0 +1,46 @@ +{ + pkgs, + lib, + inputs, + ... +}: +{ + imports = [ + ../../modules/installer + ]; + + isoImage.isoName = lib.mkForce "nixup-desk-os-installer.iso"; + + nixpkgs = { + hostPlatform = lib.mkDefault "x86_64-linux"; + config.allowUnfree = true; + }; + + nix = { + settings.experimental-features = ["nix-command" "flakes"]; + extraOptions = "experimental-features = nix-command flakes"; + }; + + boot = { + kernelPackages = pkgs.linuxPackages_latest; + supportedFilesystems = lib.mkForce ["btrfs" "reiserfs" "vfat" "f2fs" "xfs" "ntfs" "cifs"]; + }; + + networking.hostName = "desk-os-installer-demo"; + + services.qemuGuest.enable = true; + + services.openssh.enable = lib.mkForce false; + + # Inhibit sleep, suspend, hibernate + systemd.targets = { + sleep.enable = false; + suspend.enable = false; + hibernate.enable = false; + hybrid-sleep.enable = false; + }; + + environment.systemPackages = with pkgs; [ + git + ]; +} diff --git a/modules/installer/default.nix b/modules/installer/default.nix new file mode 100644 index 0000000..b94a0d6 --- /dev/null +++ b/modules/installer/default.nix @@ -0,0 +1,161 @@ +{ config, lib, options, pkgs, inputs, modulesPath, ... }: + +with lib; + +let + calamares-nixos-autostart = pkgs.makeAutostartItem { name = "io.calamares.calamares"; package = pkgs.calamares-nixos; }; +in +{ + imports = [ + ./iso-image.nix + (modulesPath + "/profiles/all-hardware.nix") + (modulesPath + "/profiles/base.nix") + (modulesPath + "/profiles/installation-device.nix") + ]; + + boot.loader.timeout = lib.mkForce 1; + + # Adds terminus_font for people with HiDPI displays + console.packages = options.console.packages.default ++ [ pkgs.terminus_font ]; + + # ISO naming. + isoImage.isoName = "${config.isoImage.isoBaseName}-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.iso"; + + # EFI booting + isoImage.makeEfiBootable = true; + + # USB booting + isoImage.makeUsbBootable = true; + + # Add Memtest86+ to the CD. + boot.loader.grub.memtest86.enable = true; + + # An installation media cannot tolerate a host config defined file + # system layout on a fresh machine, before it has been formatted. + swapDevices = mkImageMediaOverride [ ]; + fileSystems = mkImageMediaOverride config.lib.isoFileSystems; + + boot.postBootCommands = '' + for o in $( $out/EFI/boot/grub.cfg + + set textmode=${boolToString (config.isoImage.forceTextMode)} + set timeout=${toString grubEfiTimeout} + + clear + # This message will only be viewable on the default (UEFI) console. + echo "" + echo "Loading graphical boot menu..." + echo "" + echo "Press 't' to use the text boot menu on this console..." + echo "" + + ${grubMenuCfg} + + hiddenentry 'Text mode' --hotkey 't' { + loadfont (\$root)/EFI/boot/unicode.pf2 + set textmode=true + terminal_output console + } + hiddenentry 'GUI mode' --hotkey 'g' { + $(find ${config.isoImage.grubTheme} -iname '*.pf2' -printf "loadfont (\$root)/EFI/boot/grub-theme/%P\n") + set textmode=false + terminal_output gfxterm + } + + + # If the parameter iso_path is set, append the findiso parameter to the kernel + # line. We need this to allow the nixos iso to be booted from grub directly. + if [ \''${iso_path} ] ; then + set isoboot="findiso=\''${iso_path}" + fi + + # + # Menu entries + # + + ${buildMenuGrub2} + submenu "HiDPI, Quirks and Accessibility" --class hidpi --class submenu { + ${grubMenuCfg} + submenu "Suggests resolution @720p" --class hidpi-720p { + ${grubMenuCfg} + ${buildMenuAdditionalParamsGrub2 "video=1280x720@60"} + } + submenu "Suggests resolution @1080p" --class hidpi-1080p { + ${grubMenuCfg} + ${buildMenuAdditionalParamsGrub2 "video=1920x1080@60"} + } + + # If we boot into a graphical environment where X is autoran + # and always crashes, it makes the media unusable. Allow the user + # to disable this. + submenu "Disable display-manager" --class quirk-disable-displaymanager { + ${grubMenuCfg} + ${buildMenuAdditionalParamsGrub2 "systemd.mask=display-manager.service"} + } + + # Some laptop and convertibles have the panel installed in an + # inconvenient way, rotated away from the keyboard. + # Those entries makes it easier to use the installer. + submenu "" {return} + submenu "Rotate framebuffer Clockwise" --class rotate-90cw { + ${grubMenuCfg} + ${buildMenuAdditionalParamsGrub2 "fbcon=rotate:1"} + } + submenu "Rotate framebuffer Upside-Down" --class rotate-180 { + ${grubMenuCfg} + ${buildMenuAdditionalParamsGrub2 "fbcon=rotate:2"} + } + submenu "Rotate framebuffer Counter-Clockwise" --class rotate-90ccw { + ${grubMenuCfg} + ${buildMenuAdditionalParamsGrub2 "fbcon=rotate:3"} + } + + # As a proof of concept, mainly. (Not sure it has accessibility merits.) + submenu "" {return} + submenu "Use black on white" --class accessibility-blakconwhite { + ${grubMenuCfg} + ${buildMenuAdditionalParamsGrub2 "vt.default_red=0xFF,0xBC,0x4F,0xB4,0x56,0xBC,0x4F,0x00,0xA1,0xCF,0x84,0xCA,0x8D,0xB4,0x84,0x68 vt.default_grn=0xFF,0x55,0xBA,0xBA,0x4D,0x4D,0xB3,0x00,0xA0,0x8F,0xB3,0xCA,0x88,0x93,0xA4,0x68 vt.default_blu=0xFF,0x58,0x5F,0x58,0xC5,0xBD,0xC5,0x00,0xA8,0xBB,0xAB,0x97,0xBD,0xC7,0xC5,0x68"} + } + + # Serial access is a must! + submenu "" {return} + submenu "Serial console=ttyS0,115200n8" --class serial { + ${grubMenuCfg} + ${buildMenuAdditionalParamsGrub2 "console=ttyS0,115200n8"} + } + } + + ${lib.optionalString (refindBinary != null) '' + # GRUB apparently cannot do "chainloader" operations on "CD". + if [ "\$root" != "cd0" ]; then + menuentry 'rEFInd' --class refind { + # Force root to be the FAT partition + # Otherwise it breaks rEFInd's boot + search --set=root --no-floppy --fs-uuid 1234-5678 + chainloader (\$root)/EFI/boot/${refindBinary} + } + fi + ''} + menuentry 'Firmware Setup' --class settings { + fwsetup + clear + echo "" + echo "If you see this message, your EFI system doesn't support this feature." + echo "" + } + menuentry 'Shutdown' --class shutdown { + halt + } + EOF + + grub-script-check $out/EFI/boot/grub.cfg + + ${refind} + ''; + + efiImg = pkgs.runCommand "efi-image_eltorito" { + nativeBuildInputs = [ pkgs.buildPackages.mtools pkgs.buildPackages.libfaketime pkgs.buildPackages.dosfstools ]; + strictDeps = true; + } + # Be careful about determinism: du --apparent-size, + # dates (cp -p, touch, mcopy -m, faketime for label), IDs (mkfs.vfat -i) + '' + mkdir ./contents && cd ./contents + mkdir -p ./EFI/boot + cp -rp "${efiDir}"/EFI/boot/{grub.cfg,*.efi} ./EFI/boot + + # Rewrite dates for everything in the FS + find . -exec touch --date=2000-01-01 {} + + + # Round up to the nearest multiple of 1MB, for more deterministic du output + usage_size=$(( $(du -s --block-size=1M --apparent-size . | tr -cd '[:digit:]') * 1024 * 1024 )) + # Make the image 110% as big as the files need to make up for FAT overhead + image_size=$(( ($usage_size * 110) / 100 )) + # Make the image fit blocks of 1M + block_size=$((1024*1024)) + image_size=$(( ($image_size / $block_size + 1) * $block_size )) + echo "Usage size: $usage_size" + echo "Image size: $image_size" + truncate --size=$image_size "$out" + mkfs.vfat --invariant -i 12345678 -n EFIBOOT "$out" + + # Force a fixed order in mcopy for better determinism, and avoid file globbing + for d in $(find EFI -type d | sort); do + faketime "2000-01-01 00:00:00" mmd -i "$out" "::/$d" + done + + for f in $(find EFI -type f | sort); do + mcopy -pvm -i "$out" "$f" "::/$f" + done + + # Verify the FAT partition. + fsck.vfat -vn "$out" + ''; # */ + +in + +{ + options = { + + isoImage.isoName = mkOption { + default = "${config.isoImage.isoBaseName}.iso"; + type = lib.types.str; + description = '' + Name of the generated ISO image file. + ''; + }; + + isoImage.isoBaseName = mkOption { + default = config.system.nixos.distroId; + type = lib.types.str; + description = '' + Prefix of the name of the generated ISO image file. + ''; + }; + + isoImage.compressImage = mkOption { + default = false; + type = lib.types.bool; + description = '' + Whether the ISO image should be compressed using + {command}`zstd`. + ''; + }; + + isoImage.squashfsCompression = mkOption { + default = with pkgs.stdenv.hostPlatform; "xz -Xdict-size 100% " + + lib.optionalString isx86 "-Xbcj x86" + # Untested but should also reduce size for these platforms + + lib.optionalString isAarch "-Xbcj arm" + + lib.optionalString (isPower && is32bit && isBigEndian) "-Xbcj powerpc" + + lib.optionalString (isSparc) "-Xbcj sparc"; + type = lib.types.nullOr lib.types.str; + description = '' + Compression settings to use for the squashfs nix store. + `null` disables compression. + ''; + example = "zstd -Xcompression-level 6"; + }; + + isoImage.edition = mkOption { + default = ""; + type = lib.types.str; + description = '' + Specifies which edition string to use in the volume ID of the generated + ISO image. + ''; + }; + + isoImage.volumeID = mkOption { + # nixos-$EDITION-$RELEASE-$ARCH + default = "nixos${optionalString (config.isoImage.edition != "") "-${config.isoImage.edition}"}-${config.system.nixos.release}-${pkgs.stdenv.hostPlatform.uname.processor}"; + type = lib.types.str; + description = '' + Specifies the label or volume ID of the generated ISO image. + Note that the label is used by stage 1 of the boot process to + mount the CD, so it should be reasonably distinctive. + ''; + }; + + isoImage.contents = mkOption { + example = literalExpression '' + [ { source = pkgs.memtest86 + "/memtest.bin"; + target = "boot/memtest.bin"; + } + ] + ''; + description = '' + This option lists files to be copied to fixed locations in the + generated ISO image. + ''; + }; + + isoImage.storeContents = mkOption { + example = literalExpression "[ pkgs.stdenv ]"; + description = '' + This option lists additional derivations to be included in the + Nix store in the generated ISO image. + ''; + }; + + isoImage.includeSystemBuildDependencies = mkOption { + default = false; + type = lib.types.bool; + description = '' + Set this option to include all the needed sources etc in the + image. It significantly increases image size. Use that when + you want to be able to keep all the sources needed to build your + system or when you are going to install the system on a computer + with slow or non-existent network connection. + ''; + }; + + isoImage.makeBiosBootable = mkOption { + # Before this option was introduced, images were BIOS-bootable if the + # hostPlatform was x86-based. This option is enabled by default for + # backwards compatibility. + # + # Also note that syslinux package currently cannot be cross-compiled from + # non-x86 platforms, so the default is false on non-x86 build platforms. + default = pkgs.stdenv.buildPlatform.isx86 && pkgs.stdenv.hostPlatform.isx86; + defaultText = lib.literalMD '' + `true` if both build and host platforms are x86-based architectures, + e.g. i686 and x86_64. + ''; + type = lib.types.bool; + description = '' + Whether the ISO image should be a BIOS-bootable disk. + ''; + }; + + isoImage.makeEfiBootable = mkOption { + default = false; + type = lib.types.bool; + description = '' + Whether the ISO image should be an EFI-bootable volume. + ''; + }; + + isoImage.makeUsbBootable = mkOption { + default = false; + type = lib.types.bool; + description = '' + Whether the ISO image should be bootable from CD as well as USB. + ''; + }; + + isoImage.efiSplashImage = mkOption { + default = pkgs.fetchurl { + url = "https://raw.githubusercontent.com/NixOS/nixos-artwork/a9e05d7deb38a8e005a2b52575a3f59a63a4dba0/bootloader/efi-background.png"; + sha256 = "18lfwmp8yq923322nlb9gxrh5qikj1wsk6g5qvdh31c4h5b1538x"; + }; + description = '' + The splash image to use in the EFI bootloader. + ''; + }; + + isoImage.splashImage = mkOption { + default = pkgs.fetchurl { + url = "https://raw.githubusercontent.com/NixOS/nixos-artwork/a9e05d7deb38a8e005a2b52575a3f59a63a4dba0/bootloader/isolinux/bios-boot.png"; + sha256 = "1wp822zrhbg4fgfbwkr7cbkr4labx477209agzc0hr6k62fr6rxd"; + }; + description = '' + The splash image to use in the legacy-boot bootloader. + ''; + }; + + isoImage.grubTheme = mkOption { + default = pkgs.nixos-grub2-theme; + type = types.nullOr (types.either types.path types.package); + description = '' + The grub2 theme used for UEFI boot. + ''; + }; + + isoImage.syslinuxTheme = mkOption { + default = '' + MENU TITLE ${config.system.nixos.distroName} + MENU RESOLUTION 800 600 + MENU CLEAR + MENU ROWS 6 + MENU CMDLINEROW -4 + MENU TIMEOUTROW -3 + MENU TABMSGROW -2 + MENU HELPMSGROW -1 + MENU HELPMSGENDROW -1 + MENU MARGIN 0 + + # FG:AARRGGBB BG:AARRGGBB shadow + MENU COLOR BORDER 30;44 #00000000 #00000000 none + MENU COLOR SCREEN 37;40 #FF000000 #00E2E8FF none + MENU COLOR TABMSG 31;40 #80000000 #00000000 none + MENU COLOR TIMEOUT 1;37;40 #FF000000 #00000000 none + MENU COLOR TIMEOUT_MSG 37;40 #FF000000 #00000000 none + MENU COLOR CMDMARK 1;36;40 #FF000000 #00000000 none + MENU COLOR CMDLINE 37;40 #FF000000 #00000000 none + MENU COLOR TITLE 1;36;44 #00000000 #00000000 none + MENU COLOR UNSEL 37;44 #FF000000 #00000000 none + MENU COLOR SEL 7;37;40 #FFFFFFFF #FF5277C3 std + ''; + type = types.str; + description = '' + The syslinux theme used for BIOS boot. + ''; + }; + + isoImage.prependToMenuLabel = mkOption { + default = ""; + type = types.str; + example = "Install "; + description = '' + The string to prepend before the menu label for the NixOS system. + This will be directly prepended (without whitespace) to the NixOS version + string, like for example if it is set to `XXX`: + + `XXXNixOS 99.99-pre666` + ''; + }; + + isoImage.appendToMenuLabel = mkOption { + default = " Installer"; + type = types.str; + example = " Live System"; + description = '' + The string to append after the menu label for the NixOS system. + This will be directly appended (without whitespace) to the NixOS version + string, like for example if it is set to `XXX`: + + `NixOS 99.99-pre666XXX` + ''; + }; + + isoImage.forceTextMode = mkOption { + default = false; + type = types.bool; + example = true; + description = '' + Whether to use text mode instead of graphical grub. + A value of `true` means graphical mode is not tried to be used. + + This is useful for validating that graphics mode usage is not at the root cause of a problem with the iso image. + + If text mode is required off-handedly (e.g. for serial use) you can use the `T` key, after being prompted, to use text mode for the current boot. + ''; + }; + + }; + + # store them in lib so we can mkImageMediaOverride the + # entire file system layout in installation media (only) + config.lib.isoFileSystems = { + "/" = mkImageMediaOverride + { + fsType = "tmpfs"; + options = [ "mode=0755" ]; + }; + + # Note that /dev/root is a symlink to the actual root device + # specified on the kernel command line, created in the stage 1 + # init script. + "/iso" = mkImageMediaOverride + { device = "/dev/root"; + neededForBoot = true; + noCheck = true; + }; + + # In stage 1, mount a tmpfs on top of /nix/store (the squashfs + # image) to make this a live CD. + "/nix/.ro-store" = mkImageMediaOverride + { fsType = "squashfs"; + device = "/iso/nix-store.squashfs"; + options = [ "loop" ]; + neededForBoot = true; + }; + + "/nix/.rw-store" = mkImageMediaOverride + { fsType = "tmpfs"; + options = [ "mode=0755" ]; + neededForBoot = true; + }; + + "/nix/store" = mkImageMediaOverride + { fsType = "overlay"; + device = "overlay"; + options = [ + "lowerdir=/nix/.ro-store" + "upperdir=/nix/.rw-store/store" + "workdir=/nix/.rw-store/work" + ]; + depends = [ + "/nix/.ro-store" + "/nix/.rw-store/store" + "/nix/.rw-store/work" + ]; + }; + }; + + config = { + assertions = [ + { + # Syslinux (and isolinux) only supports x86-based architectures. + assertion = config.isoImage.makeBiosBootable -> pkgs.stdenv.hostPlatform.isx86; + message = "BIOS boot is only supported on x86-based architectures."; + } + { + assertion = !(stringLength config.isoImage.volumeID > 32); + # https://wiki.osdev.org/ISO_9660#The_Primary_Volume_Descriptor + # Volume Identifier can only be 32 bytes + message = let + length = stringLength config.isoImage.volumeID; + howmany = toString length; + toomany = toString (length - 32); + in + "isoImage.volumeID ${config.isoImage.volumeID} is ${howmany} characters. That is ${toomany} characters longer than the limit of 32."; + } + ]; + + # Don't build the GRUB menu builder script, since we don't need it + # here and it causes a cyclic dependency. + boot.loader.grub.enable = false; + + environment.systemPackages = [ grubPkgs.grub2 grubPkgs.grub2_efi ] + ++ optional (config.isoImage.makeBiosBootable) pkgs.syslinux + ; + + # In stage 1 of the boot, mount the CD as the root FS by label so + # that we don't need to know its device. We pass the label of the + # root filesystem on the kernel command line, rather than in + # `fileSystems' below. This allows CD-to-USB converters such as + # UNetbootin to rewrite the kernel command line to pass the label or + # UUID of the USB stick. It would be nicer to write + # `root=/dev/disk/by-label/...' here, but UNetbootin doesn't + # recognise that. + boot.kernelParams = + [ "root=LABEL=${config.isoImage.volumeID}" + "boot.shell_on_fail" + ]; + + fileSystems = config.lib.isoFileSystems; + + boot.initrd.availableKernelModules = [ "squashfs" "iso9660" "uas" "overlay" ]; + + boot.initrd.kernelModules = [ "loop" "overlay" ]; + + # Closures to be copied to the Nix store on the CD, namely the init + # script and the top-level system configuration directory. + isoImage.storeContents = + [ config.system.build.toplevel ] ++ + optional config.isoImage.includeSystemBuildDependencies + config.system.build.toplevel.drvPath; + + # Individual files to be included on the CD, outside of the Nix + # store on the CD. + isoImage.contents = + [ + { source = config.boot.kernelPackages.kernel + "/" + config.system.boot.loader.kernelFile; + target = "/boot/" + config.system.boot.loader.kernelFile; + } + { source = config.system.build.initialRamdisk + "/" + config.system.boot.loader.initrdFile; + target = "/boot/" + config.system.boot.loader.initrdFile; + } + { source = pkgs.writeText "version" config.system.nixos.label; + target = "/version.txt"; + } + ] ++ optionals (config.isoImage.makeBiosBootable) [ + { source = config.isoImage.splashImage; + target = "/isolinux/background.png"; + } + { source = pkgs.substituteAll { + name = "isolinux.cfg"; + src = pkgs.writeText "isolinux.cfg-in" isolinuxCfg; + bootRoot = "/boot"; + }; + target = "/isolinux/isolinux.cfg"; + } + { source = "${pkgs.syslinux}/share/syslinux"; + target = "/isolinux"; + } + ] ++ optionals config.isoImage.makeEfiBootable [ + { source = efiImg; + target = "/boot/efi.img"; + } + { source = "${efiDir}/EFI"; + target = "/EFI"; + } + { source = (pkgs.writeTextDir "grub/loopback.cfg" "source /EFI/boot/grub.cfg") + "/grub"; + target = "/boot/grub"; + } + { source = config.isoImage.efiSplashImage; + target = "/EFI/boot/efi-background.png"; + } + ] ++ optionals (config.boot.loader.grub.memtest86.enable && config.isoImage.makeBiosBootable) [ + { source = "${pkgs.memtest86plus}/memtest.bin"; + target = "/boot/memtest.bin"; + } + ] ++ optionals (config.isoImage.grubTheme != null) [ + { source = config.isoImage.grubTheme; + target = "/EFI/boot/grub-theme"; + } + ]; + + boot.loader.timeout = 10; + + # Create the ISO image. + system.build.isoImage = pkgs.callPackage "${toString pkgs.path}/nixos/lib/make-iso9660-image.nix" ({ + inherit (config.isoImage) isoName compressImage volumeID contents; + bootable = config.isoImage.makeBiosBootable; + bootImage = "/isolinux/isolinux.bin"; + syslinux = if config.isoImage.makeBiosBootable then pkgs.syslinux else null; + squashfsContents = config.isoImage.storeContents; + squashfsCompression = config.isoImage.squashfsCompression; + } // optionalAttrs (config.isoImage.makeUsbBootable && config.isoImage.makeBiosBootable) { + usbBootable = true; + isohybridMbrImage = "${pkgs.syslinux}/share/syslinux/isohdpfx.bin"; + } // optionalAttrs config.isoImage.makeEfiBootable { + efiBootable = true; + efiBootImage = "boot/efi.img"; + }); + + boot.postBootCommands = + '' + # After booting, register the contents of the Nix store on the + # CD in the Nix database in the tmpfs. + ${config.nix.package.out}/bin/nix-store --load-db < /nix/store/nix-path-registration + + # nixos-rebuild also requires a "system" profile and an + # /etc/NIXOS tag. + touch /etc/NIXOS + ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system + ''; + + # Add vfat support to the initrd to enable people to copy the + # contents of the CD to a bootable USB stick. + boot.initrd.supportedFilesystems = [ "vfat" ]; + + }; + +}