{ description = "A Nix library to create and manage virtual machines running Windows."; inputs.nixpkgs.url = github:NixOS/nixpkgs/nixos-21.11; outputs = { self, nixpkgs }: let pkgs = import nixpkgs { system = "x86_64-linux"; }; # common settings baseRtc = "2020-04-20T14:21:42"; cores = "4"; qemuMem = "4G"; efi = true; # utils utils = rec { # qemu_test is a smaller closure only building for a single system arch qemu = pkgs.qemu; mkQemuFlags = extraFlags: [ "-enable-kvm" "-cpu host" "-smp ${cores}" "-m ${qemuMem}" "-M q35" "-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" ] ++ extraFlags; # 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" '' set -e ${pkgs.sshpass}/bin/sshpass -p1234 -- \ ${pkgs.openssh}/bin/ssh -np 2022 ${sshOpts} \ wfvm@localhost \ $1 ''; win-wait = pkgs.writeShellScriptBin "win-wait" '' set -e # If the machine is not up within 10 minutes it's likely never coming up timeout=600 # Wait for VM to be accessible sleep 20 echo "Waiting for SSH..." while true; do if test "$timeout" -eq 0; then echo "SSH connection timed out" exit 1 fi output=$(${win-exec}/bin/win-exec 'echo|set /p="Ran command"' || echo "") if test "$output" = "Ran command"; then break fi echo "Retrying in 1 second, timing out in $timeout seconds" ((timeout=$timeout-1)) sleep 1 done echo "SSH OK" ''; win-put = pkgs.writeShellScriptBin "win-put" '' set -e echo win-put $1 -\> $2 ${pkgs.sshpass}/bin/sshpass -p1234 -- \ ${pkgs.openssh}/bin/sftp -r -P 2022 ${sshOpts} \ wfvm@localhost -b- << EOF cd $2 put $1 EOF ''; win-get = pkgs.writeShellScriptBin "win-get" '' set -e echo win-get $1 ${pkgs.sshpass}/bin/sshpass -p1234 -- \ ${pkgs.openssh}/bin/sftp -r -P 2022 ${sshOpts} \ wfvm@localhost:$1 . ''; wfvm-run = { name, image, script, display ? false, isolateNetwork ? true, forwardedPorts ? [], fakeRtc ? true }: let restrict = if isolateNetwork then "on" else "off"; # use socat instead of `tcp:...` to allow multiple connections guestfwds = builtins.concatStringsSep "" (map ({ listenAddr, targetAddr, port }: ",guestfwd=tcp:${listenAddr}:${toString port}-cmd:${pkgs.socat}/bin/socat\\ -\\ tcp:${targetAddr}:${toString port}" ) forwardedPorts); qemuParams = mkQemuFlags (pkgs.lib.optional (!display) "-display none" ++ pkgs.lib.optional (!fakeRtc) "-rtc base=localtime" ++ [ "-drive" "file=${image},index=0,media=disk,cache=unsafe" "-snapshot" "-netdev user,id=n1,net=192.168.1.0/24,restrict=${restrict},hostfwd=tcp::2022-:22${guestfwds}" ]); in pkgs.writeShellScriptBin "wfvm-run-${name}" '' set -e -m ${qemu}/bin/qemu-system-x86_64 ${pkgs.lib.concatStringsSep " " qemuParams} & ${win-wait}/bin/win-wait ${script} echo "Shutting down..." ${win-exec}/bin/win-exec 'shutdown /s' echo "Waiting for VM to terminate..." fg echo "Done" ''; }; # end of utils # ============ # layers layers = { anaconda3 = { name = "Anaconda3"; script = let Anaconda3 = pkgs.fetchurl { name = "Anaconda3.exe"; url = "https://repo.anaconda.com/archive/Anaconda3-2021.05-Windows-x86_64.exe"; sha256 = "1lpk7k4gydyk524z1nk4rrninrwi20g2ias2njc9w0a40hwl5nwk"; }; in '' ln -s ${Anaconda3} ./Anaconda3.exe win-put Anaconda3.exe . echo Running Anaconda installer... win-exec 'start /wait "" .\Anaconda3.exe /S /D=%UserProfile%\Anaconda3' echo Anaconda installer finished ''; }; msys2 = { name = "MSYS2"; buildInputs = [ pkgs.expect ]; script = let msys2 = pkgs.fetchurl { name = "msys2.exe"; url = "https://github.com/msys2/msys2-installer/releases/download/2020-06-02/msys2-x86_64-20200602.exe"; sha256 = "1mswlfybvk42vdr4r85dypgkwhrp5ff47gcbxgjqwq86ym44xzd4"; }; msys2-auto-install = pkgs.fetchurl { url = "https://raw.githubusercontent.com/msys2/msys2-installer/7b4b35f65904d03399d5dfb8fc4e5729b0b4d81f/auto-install.js"; sha256 = "17fq1xprbs00j8wb4m0w1x4dvb48qb5hwa3zx77snlhw8226d81y"; }; in '' ln -s ${msys2} ./msys2.exe ln -s ${msys2-auto-install} ./auto-install.js win-put msys2.exe . win-put auto-install.js . echo Running MSYS2 installer... # work around MSYS2 installer bug that prevents it from closing at the end of unattended install expect -c 'set timeout 600; spawn win-exec ".\\msys2.exe --script auto-install.js -v InstallPrefix=C:\\msys64"; expect FinishedPageCallback { close }' echo MSYS2 installer finished ''; }; msys2-packages = msys-packages: { name = "MSYS2-packages"; script = let msys-packages-put = pkgs.lib.strings.concatStringsSep "\n" (map (package: ''win-put ${package} 'msyspackages' '') msys-packages); in # Windows command line is so shitty it can't even do glob expansion. Why do people use Windows? '' win-exec 'mkdir msyspackages' ${msys-packages-put} cat > installmsyspackages.bat << EOF set MSYS=c:\msys64 set ARCH=64 set PATH=%MSYS%\usr\bin;%MSYS%\mingw%ARCH%\bin;%PATH% bash -c "pacman -U --noconfirm C:/Users/wfvm/msyspackages/*" EOF win-put installmsyspackages.bat . win-exec installmsyspackages ''; }; msvc = { # Those instructions are vaguely correct: # https://docs.microsoft.com/en-us/visualstudio/install/create-an-offline-installation-of-visual-studio?view=vs-2019 name = "MSVC"; script = let bootstrapper = pkgs.fetchurl { name = "RESTRICTDIST-vs_Community.exe"; url = "https://aka.ms/vs/16/release/vs_community.exe"; sha256 = "0b3csxz0qsafnvc0d74ywfpralwz8chv4zf9k07akpm8lp8ycgq0"; }; # This touchy-feely "community" piece of trash seems deliberately crafted to break Wine, so we use the VM to run it. download-vs = utils.wfvm-run { name = "download-vs"; image = makeWindowsImage { }; isolateNetwork = false; script = '' ln -s ${bootstrapper} vs_Community.exe ${utils.win-put}/bin/win-put vs_Community.exe rm vs_Community.exe ${utils.win-exec}/bin/win-exec "vs_Community.exe --quiet --norestart --layout c:\vslayout --add Microsoft.VisualStudio.Workload.NativeDesktop --includeRecommended --lang en-US" ${utils.win-get}/bin/win-get /c:/vslayout ''; }; cache = pkgs.stdenv.mkDerivation { name = "RESTRICTDIST-vs"; outputHashAlgo = "sha256"; outputHashMode = "recursive"; outputHash = "0ic3jvslp2y9v8yv9mfr2mafkvj2q5frmcyhmlbxj71si1x3kpag"; phases = [ "buildPhase" ]; buildInputs = [ download-vs ]; buildPhase = '' mkdir $out cd $out wfvm-run-download-vs ''; }; in '' ln -s ${cache}/vslayout vslayout win-put vslayout /c:/ echo "Running Visual Studio installer" win-exec "cd \vslayout && start /wait vs_Community.exe --passive --wait && echo %errorlevel%" ''; }; # You need to run the IDE at least once or else most of the Visual Studio trashware won't actually work. # With the /ResetSettings flag, it will actually start without pestering you about opening a Microsoft account. msvc-ide-unbreak = { name = "MSVC-ide-unbreak"; script = '' win-exec 'cd "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE" && devenv /ResetSettings' sleep 40 ''; }; # Disable the Windows firewall disable-firewall = { name = "disable-firewall"; script = '' echo Disabling firewall win-exec "netsh advfirewall set allprofiles state off" ''; }; # Disable automatic power management which causes the machine to go # into standby after periods without mouse wiggling. disable-autosleep = { name = "disable-autosleep"; script = '' echo Disabling autosleep win-exec "powercfg /x -hibernate-timeout-ac 0" win-exec "powercfg /x -hibernate-timeout-dc 0" win-exec "powercfg /x -disk-timeout-ac 0" win-exec "powercfg /x -disk-timeout-dc 0" win-exec "powercfg /x -monitor-timeout-ac 0" win-exec "powercfg /x -monitor-timeout-dc 0" win-exec "powercfg /x -standby-timeout-ac 0" win-exec "powercfg /x -standby-timeout-dc 0" ''; }; # Turn off automatic locking of idle user sessions disable-autolock = { name = "disable-autolock"; script = '' echo Disabling autolock win-exec "reg add HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\Personalization /v NoLockScreen /t REG_DWORD /d 1" ''; }; # Don't let Windows start completely rewriting gigabytes of disk # space. Defragmentation increases the size of our qcow layers # needlessly. disable-scheduled-defrag = { name = "disable-scheduled-defrag"; script = '' echo Disabling scheduled defragmentation service win-exec 'schtasks /Change /DISABLE /TN "\Microsoft\Windows\Defrag\ScheduledDefrag"' ''; }; # Chain together layers that are quick to run so that the VM does # not have to be started/shutdown for each. collapseLayers = scripts: { name = pkgs.lib.concatMapStringsSep "-" ({ name, ... }: name) scripts; script = builtins.concatStringsSep "\n" ( map ({ script, ... }: script) scripts ); buildInputs = builtins.concatMap ({ buildInputs ? [], ... }: buildInputs) scripts; }; }; # end of layers # ============ # autounattend build-autounattend = { fullName ? "John Doe" , organization ? "KVM Authority" , administratorPassword ? "123456" , uiLanguage ? "en-US" , inputLocale ? "en-US" , userLocale ? "en-US" , systemLocale ? "en-US" , users ? { wfvm = { password = "1234"; description = "WFVM Administrator"; groups = [ "Administrators" ]; }; } , productKey ? null , defaultUser ? "wfvm" , setupCommands ? [] , timeZone ? "UTC" , services ? {} , impureShellCommands ? [] , driveLetter ? "D:" , imageSelection ? "Windows 10 Pro" , ... }: let lib = pkgs.lib; serviceCommands = lib.mapAttrsToList ( serviceName: attrs: "powershell Set-Service -Name ${serviceName} " + ( lib.concatStringsSep " " ( ( lib.mapAttrsToList ( n: v: if builtins.typeOf v != "bool" then "-${n} ${v}" else "-${n}" ) ) ( # Always run without interaction { Force = true; } // attrs ) ) ) ) services; sshSetupCommands = # let # makeDirs = lib.mapAttrsToList (n: v: ''mkdir C:\Users\${n}\.ssh'') users; # writeKeys = lib.flatten (lib.mapAttrsToList (n: v: builtins.map (key: let # commands = [ # ''powershell.exe Set-Content -Path C:\Users\${n}\.ssh\authorized_keys -Value '${key}' '' # ]; # in lib.concatStringsSep "\n" commands) (v.sshKeys or [])) users); # mkDirsDesc = builtins.map (c: {Path = c; Description = "Make SSH key dir";}) makeDirs; # writeKeysDesc = builtins.map (c: {Path = c; Description = "Add SSH key";}) writeKeys; # in # mkDirsDesc ++ writeKeysDesc ++ [ { Path = ''powershell.exe ${driveLetter}\install-ssh.ps1''; Description = "Install OpenSSH service."; } ]; 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."; } ] ++ setupCommands ++ [ { Path = ''powershell.exe ${driveLetter}\setup.ps1''; Description = "Setup SSH and keys"; } ] ++ serviceCommands ++ impureShellCommands ); mkCommand = attrs: '' ${lib.concatStringsSep "\n" (lib.attrsets.mapAttrsToList (n: v: "<${n}>${v}") attrs)} ''; mkCommands = commands: ( builtins.foldl' ( acc: v: rec { i = acc.i + 1; values = acc.values ++ [ (mkCommand (v // { Order = builtins.toString i; })) ]; } ) { i = 0; values = []; } commands ).values; mkUser = { name , password , description ? "" , displayName ? "" , groups ? [] # , sshKeys ? [] # Handled in scripts }: '' ${password} true</PlainText> </Password> <Description>${description}</Description> <DisplayName>${displayName}</DisplayName> <Group>${builtins.concatStringsSep ";" (lib.unique ([ "Users" ] ++ groups))}</Group> <Name>${name}</Name> </LocalAccount> ''; # 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; autounattendXML = pkgs.writeText "autounattend.xml" '' <?xml version="1.0" encoding="utf-8"?> <unattend xmlns="urn:schemas-microsoft-com:unattend"> <settings pass="windowsPE"> <component name="Microsoft-Windows-PnpCustomizationsWinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <DriverPaths> <PathAndCredentials wcm:action="add" wcm:keyValue="1"> <Path>D:\</Path> </PathAndCredentials> <PathAndCredentials wcm:action="add" wcm:keyValue="2"> <Path>E:\</Path> </PathAndCredentials> <PathAndCredentials wcm:action="add" wcm:keyValue="3"> <Path>C:\virtio\amd64\w10</Path> </PathAndCredentials> <PathAndCredentials wcm:action="add" wcm:keyValue="4"> <Path>C:\virtio\NetKVM\w10\amd64</Path> </PathAndCredentials> <PathAndCredentials wcm:action="add" wcm:keyValue="5"> <Path>C:\virtio\qxldod\w10\amd64</Path> </PathAndCredentials> </DriverPaths> </component> <component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <DiskConfiguration> <Disk wcm:action="add"> <CreatePartitions> <CreatePartition wcm:action="add"> <Order>1</Order> <Type>${if efi then "EFI" else "Primary"}</Type> <Size>300</Size> </CreatePartition> <CreatePartition wcm:action="add"> <Order>2</Order> <Type>${if efi then "MSR" else "Primary"}</Type> <Size>16</Size> </CreatePartition> <CreatePartition wcm:action="add"> <Order>3</Order> <Type>Primary</Type> <Extend>true</Extend> </CreatePartition> </CreatePartitions> <ModifyPartitions> <ModifyPartition wcm:action="add"> <Order>1</Order> <Format>${if efi then "FAT32" else "NTFS"}</Format> <Label>System</Label> <PartitionID>1</PartitionID> </ModifyPartition> <ModifyPartition wcm:action="add"> <Order>2</Order> <PartitionID>2</PartitionID> </ModifyPartition> <ModifyPartition wcm:action="add"> <Order>3</Order> <Format>NTFS</Format> <Label>Windows</Label> <Letter>C</Letter> <PartitionID>3</PartitionID> </ModifyPartition> </ModifyPartitions> <DiskID>${toString diskId}</DiskID> <WillWipeDisk>true</WillWipeDisk> </Disk> </DiskConfiguration> <ImageInstall> <OSImage> <InstallTo> <DiskID>${toString diskId}</DiskID> <PartitionID>3</PartitionID> </InstallTo> <InstallFrom> <MetaData wcm:action="add"> <Key>/IMAGE/NAME</Key> <Value>${imageSelection}</Value> </MetaData> </InstallFrom> </OSImage> </ImageInstall> <UserData> <ProductKey> ${if productKey != null then "<Key>${productKey}</Key>" else ""} <WillShowUI>OnError</WillShowUI> </ProductKey> <AcceptEula>true</AcceptEula> <FullName>${fullName}</FullName> <Organization>${organization}</Organization> </UserData> </component> <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <SetupUILanguage> <UILanguage>${uiLanguage}</UILanguage> </SetupUILanguage> <InputLocale>${inputLocale}</InputLocale> <SystemLocale>${systemLocale}</SystemLocale> <UILanguage>${uiLanguage}</UILanguage> <UILanguageFallback>en-US</UILanguageFallback> <UserLocale>${userLocale}</UserLocale> </component> </settings> <settings pass="oobeSystem"> <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <InputLocale>${inputLocale}</InputLocale> <SystemLocale>${systemLocale}</SystemLocale> <UILanguage>${uiLanguage}</UILanguage> <UILanguageFallback>en-US</UILanguageFallback> <UserLocale>${userLocale}</UserLocale> </component> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <OOBE> <HideEULAPage>true</HideEULAPage> <HideLocalAccountScreen>true</HideLocalAccountScreen> <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen> <HideOnlineAccountScreens>true</HideOnlineAccountScreens> <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> <ProtectYourPC>1</ProtectYourPC> </OOBE> <TimeZone>${timeZone}</TimeZone> <UserAccounts> ${if administratorPassword != null then '' <AdministratorPassword> <Value>${administratorPassword}</Value> <PlainText>true</PlainText> </AdministratorPassword> '' else ""} <LocalAccounts> ${builtins.concatStringsSep "\n" (builtins.map mkUser flatUsers)} </LocalAccounts> </UserAccounts> ${if defaultUser == null then "" else '' <AutoLogon> <Password> <Value>${(builtins.getAttr defaultUser users).password}</Value> <PlainText>true</PlainText> </Password> <Enabled>true</Enabled> <Username>${defaultUser}</Username> </AutoLogon> ''} </component> <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Reseal> <ForceShutdownNow>true</ForceShutdownNow> <Mode>OOBE</Mode> </Reseal> </component> </settings> <settings pass="specialize"> <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <RunSynchronous> ${lib.concatStringsSep "\n" (mkCommands commands)} </RunSynchronous> </component> <component name="Microsoft-Windows-SQMApi" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="NonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <CEIPEnabled>0</CEIPEnabled> </component> </settings> <!-- Disable Windows UAC --> <settings pass="offlineServicing"> <component name="Microsoft-Windows-LUA-Settings" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <EnableLUA>false</EnableLUA> </component> </settings> <cpi:offlineImage cpi:source="wim:c:/wim/windows-10/install.wim#${imageSelection}" xmlns:cpi="urn:schemas-microsoft-com:cpi" /> </unattend> ''; in { # Lint and format as a sanity check autounattendXML = pkgs.runCommandNoCC "autounattend.xml" {} '' ${pkgs.libxml2}/bin/xmllint --format ${autounattendXML} > $out ''; # autounattend.xml is _super_ picky about quotes and other things setupScript = pkgs.writeText "setup.ps1" ( '' # Setup SSH and keys '' + lib.concatStrings ( builtins.map (c: '' # ${c.Description} ${c.Path} '') sshSetupCommands ) ); }; # /autounattend ============ # bundle bundleInstaller = pkgs.runCommandNoCC "win-bundle-installer.exe" {} '' mkdir bundle cd bundle cp ${./bundle/go.mod} go.mod cp ${./bundle/main.go} main.go env HOME=$(mktemp -d) GOOS=windows GOARCH=amd64 ${pkgs.go}/bin/go build mv bundle.exe $out ''; # /bundle =========== # makeWindowsImage makeWindowsImage = { diskImageSize ? "70G", windowsImage ? null, autoUnattendParams ? {} , impureMode ? false, installCommands ? [] , users ? {} # autounattend always installs index 1, so this default is backward-compatible , imageSelection ? "Windows 10 Pro" , ... }@attrs: let lib = pkgs.lib; libguestfs = pkgs.libguestfs-with-appliance; # p7zip on >20.03 has known vulns but we have no better option p7zip = pkgs.p7zip.overrideAttrs(old: { meta = old.meta // { knownVulnerabilities = []; }; }); runQemuCommand = name: command: ( pkgs.runCommandNoCC name { buildInputs = [ p7zip utils.qemu libguestfs ]; } ( '' if ! test -f; then echo "KVM not available, bailing out" >> /dev/stderr exit 1 fi '' + command ) ); 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"; }; # 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"; }; openSshServerPackage = pkgs.fetchurl { url = "https://github.com/PowerShell/Win32-OpenSSH/releases/download/V8.6.0.0p1-Beta/OpenSSH-Win64.zip"; sha256 = "1dw6n054r0939501dpxfm7ghv21ihmypdx034van8cl21gf1b4lz"; }; autounattend = build-autounattend attrs // { users = users // { wfvm = { password = "1234"; description = "WFVM Administrator"; groups = [ "Administrators" ]; }; }; }; # Packages required to drive installation of other packages bootstrapPkgs = runQemuCommand "bootstrap-win-pkgs.img" '' 7z x -y ${virtioWinIso} -opkgs/virtio cp ${bundleInstaller} pkgs/"$(stripHash "${bundleInstaller}")" # Install optional windows features cp ${openSshServerPackage} pkgs/OpenSSH-Win64.zip # SSH setup script goes here because windows XML parser sucks cp ${self}/install-ssh.ps1 pkgs/install-ssh.ps1 cp ${autounattend.setupScript} pkgs/setup.ps1 virt-make-fs --partition --type=fat pkgs/ $out ''; installScript = pkgs.writeScript "windows-install-script" ( let qemuParams = utils.mkQemuFlags (lib.optional (!impureMode) "-display none" ++ [ # "CD" drive with bootstrap pkgs "-drive" "id=virtio-win,file=${bootstrapPkgs},if=none,format=raw,readonly=on" "-device" "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"}" "-device" "usb-storage,drive=win-install" # Output image "-drive" "file=c.img,index=0,media=disk,if=virtio,cache=unsafe" # Network "-netdev user,id=n1,net=192.168.1.0/24,restrict=on" ]); in '' #!${pkgs.runtimeShell} set -euxo pipefail export PATH=${lib.makeBinPath [ p7zip utils.qemu libguestfs pkgs.wimlib ]}:$PATH # Create a bootable "USB" image # Booting in USB mode circumvents the "press any key to boot from cdrom" prompt # # Also embed the autounattend answer file in this image mkdir -p win mkdir -p win/nix-win 7z x -y ${windowsIso} -owin # Split image so it fits in FAT32 partition wimsplit win/sources/install.wim win/sources/install.swm 4090 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 # 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" {} '' ${installScript} mv c.img $out ''; finalImage = builtins.foldl' (acc: v: pkgs.runCommandNoCC "RESTRICTDIST-${v.name}.img" { buildInputs = with utils; [ qemu win-wait win-exec win-put ] ++ (v.buildInputs or []); } (let script = pkgs.writeScript "${v.name}-script" v.script; qemuParams = utils.mkQemuFlags (lib.optional (!impureMode) "-display none" ++ [ # Output image "-drive" "file=c.img,index=0,media=disk,if=virtio,cache=unsafe" # Network - enable SSH forwarding "-netdev user,id=n1,net=192.168.1.0/24,restrict=on,hostfwd=tcp::2022-:22" ]); in '' # Create an image referencing the previous image in the chain qemu-img create -f qcow2 -b ${acc} c.img set -m qemu-system-x86_64 ${lib.concatStringsSep " " qemuParams} & win-wait echo "Executing script to build layer..." ${script} echo "Layer script done" echo "Shutting down..." win-exec 'shutdown /s' echo "Waiting for VM to terminate..." fg echo "Done" mv c.img $out '')) baseImage ( [ { name = "DisablePasswordExpiry"; script = '' win-exec 'wmic UserAccount set PasswordExpires=False' ''; } ] ++ installCommands ); in if !(impureMode) then finalImage else assert installCommands == []; installScript; # end of makeWindowsImage build-demo-image = { impureMode ? false }: makeWindowsImage { # Build install script & skip building iso # 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"; # }; # impureShellCommands = [ # "powershell.exe echo Hello" # ]; # User accounts # users = { # artiq = { # password = "1234"; # # description = "Default user"; # # displayName = "Display name"; # groups = [ # "Administrators" # ]; # }; # }; # Auto login # defaultUser = "artiq"; # fullName = "M-Labs"; # organization = "m-labs"; # administratorPassword = "12345"; # Imperative installation commands, to be installed incrementally installCommands = with layers; [ (collapseLayers [ disable-autosleep disable-autolock disable-firewall ]) anaconda3 msys2 msvc msvc-ide-unbreak ]; # services = { # # Enable remote management # WinRm = { # Status = "Running"; # PassThru = true; # }; # }; # License key (required) # productKey = throw "Search the f* web" imageSelection = "Windows 10 Pro"; # Locales # uiLanguage = "en-US"; # inputLocale = "en-US"; # userLocale = "en-US"; # systemLocale = "en-US"; }; in { # bundle dev env devShell.x86_64-linux = pkgs.mkShell { name = "wfvm-dev-shell"; buildInputs = with pkgs; [ go ]; shellHook = '' unset GOPATH ''; }; inherit utils; inherit makeWindowsImage; demo-ssh = utils.wfvm-run { name = "demo-ssh"; image = import ./demo-image.nix { inherit pkgs; }; isolateNetwork = false; script = '' ${pkgs.sshpass}/bin/sshpass -p1234 -- ${pkgs.openssh}/bin/ssh -p 2022 wfvm@localhost -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ''; }; packages.x86_64-linux = { demo-image = build-demo-image {}; demo-image-impure = build-demo-image { impureMode = true; }; make-msys-packages = utils.wfvm-run { name = "get-msys-packages"; image = makeWindowsImage { installCommands = [ layers.msys2 ]; }; script = '' cat > getmsyspackages.bat << EOF set MSYS=C:\\MSYS64 set TOOLPREF=mingw-w64-x86_64- set PATH=%MSYS%\usr\bin;%MSYS%\mingw64\bin;%PATH% pacman -Sp %TOOLPREF%gcc %TOOLPREF%binutils make autoconf automake libtool texinfo > packages.txt EOF \${utils.win-put}/bin/win-put getmsyspackages.bat \${utils.win-exec}/bin/win-exec getmsyspackages \${utils.win-get}/bin/win-get packages.txt ''; }; }; }; }