diff --git a/README.md b/README.md index 4b8e9c8..7792f8a 100644 --- a/README.md +++ b/README.md @@ -52,3 +52,30 @@ Impure/pure mode Sometimes it can be useful to build the image _outside_ of the Nix sandbox for debugging purposes. For this purpose we have an attribute called `impureMode` which outputs the shell script used by Nix inside the sandbox to build the image. + + +Usage with Nix Flakes +--------------------- + +Build the demo by running: +```shell +nix build .#demoImage +``` + +This project's **flake.nix** exposes its functions under `lib`. To use +in your own project, setup your flake like this: + +```nix +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + wfvm.url = "git+https://git.m-labs.hk/m-labs/wfvm"; + }; + + outputs = { self, nixpkgs, wfvm }: { + packages."x86_64-linux".flaky-os = wfvm.lib.makeWindowsImage { + # configuration parameters go here + }; + }; +} +``` diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..39d0bec --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1665449268, + "narHash": "sha256-cw4xrQIAZUyJGj58Dp5VLICI0rscd+uap83afiFzlcA=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "285e77efe87df64105ec14b204de6636fb0a7a27", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..920e60f --- /dev/null +++ b/flake.nix @@ -0,0 +1,24 @@ +{ + description = "WFVM: Windows Functional Virtual Machine"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs }: + let + # only x64 is supported + system = "x86_64-linux"; + + pkgs = nixpkgs.legacyPackages.${system}; + + in { + lib = import ./wfvm { + inherit pkgs; + }; + + packages.${system}.demoImage = import ./wfvm/demo-image.nix { + inherit self; + }; + }; +} diff --git a/wfvm/autounattend.nix b/wfvm/autounattend.nix index 99b0429..d4e4dfb 100644 --- a/wfvm/autounattend.nix +++ b/wfvm/autounattend.nix @@ -14,8 +14,8 @@ , services ? {} , impureShellCommands ? [] , driveLetter ? "D:" -, efi ? true -, imageSelection ? "Windows 10 Pro" +, imageSelection ? "Windows 11 Pro N" +, enableTpm , ... }: @@ -58,18 +58,16 @@ let assertCommand = c: builtins.typeOf c == "string" || builtins.typeOf c == "set" && builtins.hasAttr "Path" c && builtins.hasAttr "Description" c; commands = builtins.map (x: assert assertCommand x; if builtins.typeOf x == "string" then { Path = x; Description = x; } else x) ( - [ - { - Path = "powershell.exe Set-ExecutionPolicy -Force Unrestricted"; - Description = "Allow unsigned powershell scripts."; - } - ] - ++ [ - { - Path = ''powershell.exe ${driveLetter}\win-bundle-installer.exe''; - Description = "Install any declared packages."; - } - ] + [ { + Path = "powershell.exe Set-ExecutionPolicy -Force Unrestricted"; + Description = "Allow unsigned powershell scripts."; + } { + Path = ''powershell.exe ${driveLetter}\win-bundle-installer.exe''; + Description = "Install any declared packages."; + } { + Path = "net accounts /maxpwage:unlimited"; + Description = "Disable forced password expiry."; + } ] ++ setupCommands ++ [ { @@ -121,8 +119,7 @@ let # Windows expects a flat list of users while we want to manage them as a set flatUsers = builtins.attrValues (builtins.mapAttrs (name: s: s // { inherit name; }) users); - diskId = - if efi then 2 else 1; + diskId = 2; autounattendXML = pkgs.writeText "autounattend.xml" '' @@ -148,18 +145,26 @@ let + ${lib.optionalString (!enableTpm) '' + + + 1 + reg add HKLM\System\Setup\LabConfig /v BypassTPMCheck /t reg_dword /d 0x00000001 /f + + + ''} 1 - ${if efi then "EFI" else "Primary"} + EFI 300 2 - ${if efi then "MSR" else "Primary"} + MSR 16 @@ -171,7 +176,7 @@ let 1 - ${if efi then "FAT32" else "NTFS"} + FAT32 1 @@ -199,6 +204,7 @@ let 3 + \install.swm /IMAGE/NAME ${imageSelection} @@ -209,7 +215,7 @@ let - ${if productKey != null then "${productKey}" else ""} + ${if productKey != null then "${productKey}" else ""} OnError true @@ -299,13 +305,13 @@ let - + ''; in { # Lint and format as a sanity check - autounattendXML = pkgs.runCommandNoCC "autounattend.xml" {} '' + autounattendXML = pkgs.runCommand "autounattend.xml" {} '' ${pkgs.libxml2}/bin/xmllint --format ${autounattendXML} > $out ''; diff --git a/wfvm/bundle/default.nix b/wfvm/bundle/default.nix index 16a0fed..c3bc5ab 100644 --- a/wfvm/bundle/default.nix +++ b/wfvm/bundle/default.nix @@ -1,6 +1,6 @@ { pkgs }: -pkgs.runCommandNoCC "win-bundle-installer.exe" {} '' +pkgs.runCommand "win-bundle-installer.exe" {} '' mkdir bundle cd bundle cp ${./go.mod} go.mod diff --git a/wfvm/demo-image.nix b/wfvm/demo-image.nix index a928de1..9c6bfb1 100644 --- a/wfvm/demo-image.nix +++ b/wfvm/demo-image.nix @@ -1,7 +1,17 @@ -{ pkgs ? import {}, impureMode ? false }: +{ pkgs ? import {} +# Whether to generate just a script to start and debug the windows installation +, impureMode ? false +# Flake input `self` +, self ? null +}: let - wfvm = (import ./default.nix { inherit pkgs; }); + wfvm = + if self == null + # nix-build + then (import ./default.nix { inherit pkgs; }) + # built from flake.nix + else self.lib; in wfvm.makeWindowsImage { # Build install script & skip building iso @@ -9,9 +19,9 @@ wfvm.makeWindowsImage { # Custom base iso # windowsImage = pkgs.requireFile rec { - # name = "Win10_21H1_English_x64.iso"; - # sha256 = "1sl51lnx4r6ckh5fii7m2hi15zh8fh7cf7rjgjq9kacg8hwyh4b9"; - # message = "Get ${name} from https://www.microsoft.com/en-us/software-download/windows10ISO"; + # name = "Win11_22H2_English_x64v1.iso"; + # sha256 = "08mbppsm1naf73z8fjyqkf975nbls7xj9n4fq0yp802dv1rz3whd"; + # message = "Get disk image ${name} from https://www.microsoft.com/en-us/software-download/windows11/"; # }; # impureShellCommands = [ @@ -60,7 +70,7 @@ wfvm.makeWindowsImage { # License key (required) # productKey = throw "Search the f* web" - imageSelection = "Windows 10 Pro"; + imageSelection = "Windows 11 Pro N"; # Locales diff --git a/wfvm/layers/default.nix b/wfvm/layers/default.nix index a9068a6..62ef39b 100644 --- a/wfvm/layers/default.nix +++ b/wfvm/layers/default.nix @@ -72,7 +72,7 @@ in bootstrapper = pkgs.fetchurl { name = "RESTRICTDIST-vs_Community.exe"; url = "https://aka.ms/vs/16/release/vs_community.exe"; - sha256 = "0b3csxz0qsafnvc0d74ywfpralwz8chv4zf9k07akpm8lp8ycgq0"; + sha256 = "sha256-l4ZKFZTgHf3BmD0eFWyGwsvb4lqB/LiQYizAABOs3gg="; }; # This touchy-feely "community" piece of trash seems deliberately crafted to break Wine, so we use the VM to run it. download-vs = wfvm.utils.wfvm-run { @@ -93,7 +93,7 @@ in outputHashAlgo = "sha256"; outputHashMode = "recursive"; - outputHash = "0ic3jvslp2y9v8yv9mfr2mafkvj2q5frmcyhmlbxj71si1x3kpag"; + outputHash = "sha256-GoOKzln8DXVMx52jWGEjwkOFkpSW+wEffAVmBVugIyk="; phases = [ "buildPhase" ]; buildInputs = [ download-vs ]; diff --git a/wfvm/utils.nix b/wfvm/utils.nix index 9039682..fee901f 100644 --- a/wfvm/utils.nix +++ b/wfvm/utils.nix @@ -1,23 +1,42 @@ -{ pkgs, baseRtc ? "2020-04-20T14:21:42", cores ? "4", qemuMem ? "4G", efi ? true }: +{ pkgs +, baseRtc ? "2022-10-10T10:10:10" +, cores ? "4" +, qemuMem ? "4G" +, enableTpm ? false +}: rec { # qemu_test is a smaller closure only building for a single system arch qemu = pkgs.qemu; + OVMF = pkgs.OVMF.override { + secureBoot = true; + }; + mkQemuFlags = extraFlags: [ "-enable-kvm" "-cpu host" "-smp ${cores}" "-m ${qemuMem}" - "-M q35" + "-M q35,smm=on" "-vga qxl" "-rtc base=${baseRtc}" "-device qemu-xhci" "-device virtio-net-pci,netdev=n1" - ] ++ pkgs.lib.optionals efi [ - "-bios ${pkgs.OVMF.fd}/FV/OVMF.fd" + "-bios ${OVMF.fd}/FV/OVMF.fd" + ] ++ pkgs.lib.optionals enableTpm [ + "-chardev" "socket,id=chrtpm,path=tpm.sock" + "-tpmdev" "emulator,id=tpm0,chardev=chrtpm" + "-device" "tpm-tis,tpmdev=tpm0" ] ++ extraFlags; + tpmStartCommands = pkgs.lib.optionalString enableTpm '' + mkdir -p tpmstate + ${pkgs.swtpm}/bin/swtpm socket \ + --tpmstate dir=tpmstate \ + --ctrl type=unixio,path=tpm.sock & + ''; + # Pass empty config file to prevent ssh from failing to create ~/.ssh sshOpts = "-F /dev/null -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=1"; win-exec = pkgs.writeShellScriptBin "win-exec" '' @@ -93,6 +112,7 @@ rec { ]); in pkgs.writeShellScriptBin "wfvm-run-${name}" '' set -e -m + ${tpmStartCommands} ${qemu}/bin/qemu-system-x86_64 ${pkgs.lib.concatStringsSep " " qemuParams} & ${win-wait}/bin/win-wait diff --git a/wfvm/win.nix b/wfvm/win.nix index 0e153e4..2319fe5 100644 --- a/wfvm/win.nix +++ b/wfvm/win.nix @@ -5,16 +5,16 @@ , impureMode ? false , installCommands ? [] , users ? {} +, enableTpm ? true # autounattend always installs index 1, so this default is backward-compatible -, imageSelection ? "Windows 10 Pro" -, efi ? true +, imageSelection ? "Windows 11 Pro N" , ... }@attrs: let lib = pkgs.lib; - utils = import ./utils.nix { inherit pkgs efi; }; - libguestfs = pkgs.libguestfs-with-appliance; + utils = import ./utils.nix { inherit pkgs enableTpm; }; + inherit (pkgs) guestfs-tools; # p7zip on >20.03 has known vulns but we have no better option p7zip = pkgs.p7zip.overrideAttrs(old: { @@ -24,7 +24,7 @@ let }); runQemuCommand = name: command: ( - pkgs.runCommandNoCC name { buildInputs = [ p7zip utils.qemu libguestfs ]; } + pkgs.runCommand name { buildInputs = [ p7zip utils.qemu guestfs-tools ]; } ( '' if ! test -f; then @@ -36,15 +36,14 @@ let ); windowsIso = if windowsImage != null then windowsImage else pkgs.requireFile rec { - name = "Win10_21H1_English_x64.iso"; - sha256 = "1sl51lnx4r6ckh5fii7m2hi15zh8fh7cf7rjgjq9kacg8hwyh4b9"; - message = "Get ${name} from https://www.microsoft.com/en-us/software-download/windows10ISO"; + name = "Win11_22H2_English_x64v2.iso"; + sha256 = "0xhhxy47yaf1jsfmskym5f65hljw8q0aqs70my86m402i6dsjnc0"; + message = "Get disk image ${name} from https://www.microsoft.com/en-us/software-download/windows11/"; }; - # stable as of 2021-04-08 virtioWinIso = pkgs.fetchurl { - url = "https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.185-2/virtio-win-0.1.185.iso"; - sha256 = "11n3kjyawiwacmi3jmfmn311g9xvfn6m0ccdwnjxw1brzb4kqaxg"; + url = "https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.229-1/virtio-win.iso"; + sha256 = "1q5vrcd70kya4nhlbpxmj7mwmwra1hm3x7w8rzkawpk06kg0v2n8"; }; openSshServerPackage = pkgs.fetchurl { @@ -54,7 +53,7 @@ let autounattend = import ./autounattend.nix ( attrs // { - inherit pkgs; + inherit pkgs enableTpm; users = users // { wfvm = { password = "1234"; @@ -96,7 +95,7 @@ let "usb-storage,drive=virtio-win" # USB boot "-drive" - "id=win-install,file=${if efi then "usb" else "cd"}image.img,if=none,format=raw,readonly=on,media=${if efi then "disk" else "cdrom"}" + "id=win-install,file=usbimage.img,if=none,format=raw,readonly=on,media=disk" "-device" "usb-storage,drive=win-install" # Output image @@ -109,7 +108,7 @@ let '' #!${pkgs.runtimeShell} set -euxo pipefail - export PATH=${lib.makeBinPath [ p7zip utils.qemu libguestfs pkgs.wimlib ]}:$PATH + export PATH=${lib.makeBinPath [ p7zip utils.qemu guestfs-tools pkgs.wimlib ]}:$PATH # Create a bootable "USB" image # Booting in USB mode circumvents the "press any key to boot from cdrom" prompt @@ -120,30 +119,28 @@ let 7z x -y ${windowsIso} -owin # Split image so it fits in FAT32 partition - wimsplit win/sources/install.wim win/sources/install.swm 4090 + wimsplit win/sources/install.wim win/sources/install.swm 4070 rm win/sources/install.wim cp ${autounattend.autounattendXML} win/autounattend.xml - ${if efi then '' virt-make-fs --partition --type=fat win/ usbimage.img - '' else '' - ${pkgs.cdrkit}/bin/mkisofs -iso-level 4 -l -R -udf -D -b boot/etfsboot.com -no-emul-boot -boot-load-size 8 -hide boot.catalog -eltorito-alt-boot -o cdimage.img win/ - ''} rm -rf win + ${utils.tpmStartCommands} + # Qemu requires files to be rw qemu-img create -f qcow2 c.img ${diskImageSize} qemu-system-x86_64 ${lib.concatStringsSep " " qemuParams} '' ); - baseImage = pkgs.runCommandNoCC "RESTRICTDIST-windows.img" {} '' + baseImage = pkgs.runCommand "RESTRICTDIST-windows.img" {} '' ${installScript} mv c.img $out ''; - finalImage = builtins.foldl' (acc: v: pkgs.runCommandNoCC "RESTRICTDIST-${v.name}.img" { + finalImage = builtins.foldl' (acc: v: pkgs.runCommand "RESTRICTDIST-${v.name}.img" { buildInputs = with utils; [ qemu win-wait win-exec win-put ] ++ (v.buildInputs or []); @@ -158,8 +155,11 @@ let ]); in '' + set -x + ${utils.tpmStartCommands} + # Create an image referencing the previous image in the chain - qemu-img create -f qcow2 -b ${acc} c.img + qemu-img create -F qcow2 -f qcow2 -b ${acc} c.img set -m qemu-system-x86_64 ${lib.concatStringsSep " " qemuParams} &