diff --git a/artiq-fast/wfvm/README.md b/artiq-fast/wfvm/README.md
new file mode 100644
index 0000000..9272098
--- /dev/null
+++ b/artiq-fast/wfvm/README.md
@@ -0,0 +1,26 @@
+# Preparation steps
+
+## Install a Windows image
+
+1. Adjust build.nix accordingly
+2. Run:
+
+If in impure mode
+```shell
+nix-build build.nix
+./result
+```
+Results in a file called c.img
+
+If in pure mode
+```shell
+nix-build build.nix
+ls -la ./result
+```
+Results in a symlink to the image in the nix store
+
+
+# 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.
diff --git a/artiq-fast/wfvm/autounattend.nix b/artiq-fast/wfvm/autounattend.nix
new file mode 100644
index 0000000..5c2d038
--- /dev/null
+++ b/artiq-fast/wfvm/autounattend.nix
@@ -0,0 +1,318 @@
+{ pkgs
+, lib ? pkgs.lib
+, fullName
+, organization
+, administratorPassword
+, uiLanguage ? "en-US"
+, inputLocale ? "en-US"
+, userLocale ? "en-US"
+, systemLocale ? "en-US"
+, users ? {}
+, productKey ? null
+, defaultUser ? null
+, setupCommands ? []
+, timeZone ? "UTC"
+, services ? {}
+, impureShellCommands ? []
+, driveLetter ? "E:"
+, ...
+}:
+
+let
+
+ 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 Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 -Source ${driveLetter}\fod -LimitAccess'';
+ Description = "Add OpenSSH service.";
+ }
+ {
+ Path = ''powershell.exe Set-Service -Name sshd -StartupType Automatic'';
+ Description = "Enable SSH by default.";
+ }
+ ];
+
+ 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}\ssh-setup.ps1'';
+ Description = "Setup SSH and keys";
+ }
+ ]
+ ++ serviceCommands
+ ++ impureShellCommands
+ );
+
+ mkCommand = attrs: ''
+
+ ${lib.concatStringsSep "\n" (lib.attrsets.mapAttrsToList (n: v: "<${n}>${v}${n}>") 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
+
+ ${description}
+ ${displayName}
+ ${builtins.concatStringsSep ";" (lib.unique ([ "Users" ] ++ groups))}
+ ${name}
+
+ '';
+
+ # 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);
+
+ autounattendXML = pkgs.writeText "autounattend.xml" ''
+
+
+
+
+
+
+ D:\
+
+
+ E:\
+
+
+
+
+
+
+
+
+
+ 1
+ EFI
+ 100
+
+
+ 2
+ MSR
+ 16
+
+
+ 3
+ Primary
+ true
+
+
+
+
+ 1
+ FAT32
+
+ 1
+
+
+ 2
+ 2
+
+
+ 3
+ NTFS
+
+ C
+ 3
+
+
+ 0
+ true
+
+
+
+
+
+
+ 0
+ 3
+
+
+
+ /IMAGE/INDEX
+ 1
+
+
+
+
+
+
+
+ ${if productKey != null then "${productKey}" else ""}
+ OnError
+
+ true
+ ${fullName}
+ ${organization}
+
+
+
+
+
+ ${uiLanguage}
+
+ ${inputLocale}
+ ${systemLocale}
+ ${uiLanguage}
+ en-US
+ ${userLocale}
+
+
+
+
+
+ ${inputLocale}
+ ${systemLocale}
+ ${uiLanguage}
+ en-US
+ ${userLocale}
+
+
+
+ true
+ true
+ true
+ true
+ true
+ 1
+
+ ${timeZone}
+
+
+ ${if administratorPassword != null then ''
+
+ ${administratorPassword}
+ true
+
+ '' else ""}
+
+ ${builtins.concatStringsSep "\n" (builtins.map mkUser flatUsers)}
+
+
+
+ ${if defaultUser == null then "" else ''
+
+
+ ${(builtins.getAttr defaultUser users).password}
+ true
+
+ true
+ ${defaultUser}
+
+ ''}
+
+
+
+ 1
+ cmd /C shutdown /s /f /t 00
+ ChangeHideFiles
+
+
+
+
+
+
+
+
+
+ ${lib.concatStringsSep "\n" (mkCommands commands)}
+
+
+
+ 0
+
+
+
+
+
+
+ false
+
+
+
+
+
+ '';
+
+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 "ssh-setup.ps1" (
+ ''
+ # Setup SSH and keys
+ '' +
+ lib.concatStrings (
+ builtins.map (c: ''
+ # ${c.Description}
+ ${c.Path}
+ '') sshSetupCommands
+ )
+ );
+
+}
diff --git a/artiq-fast/wfvm/build.nix b/artiq-fast/wfvm/build.nix
new file mode 100644
index 0000000..2a64d8f
--- /dev/null
+++ b/artiq-fast/wfvm/build.nix
@@ -0,0 +1,100 @@
+{
+ pkgs ? import {}
+ , impureMode ? false
+}:
+
+let
+ win = (import ./default.nix { inherit pkgs; });
+
+in
+win.makeWindowsImage {
+
+ # Custom base iso
+ # windowsImage = pkgs.fetchurl {
+ # url = "https://software-download.microsoft.com/download/sg/17763.107.101029-1455.rs5_release_svc_refresh_CLIENT_LTSC_EVAL_x64FRE_en-us.iso";
+ # sha256 = "668fe1af70c2f7416328aee3a0bb066b12dc6bbd2576f40f812b95741e18bc3a";
+ # };
+
+ # User accounts
+ users = {
+ artiq = {
+ password = "1234";
+ # description = "Default user";
+ # displayName = "Display name";
+ groups = [
+ "Administrators"
+ ];
+ };
+ };
+
+ # Build install script & skip building iso
+ inherit impureMode;
+
+ # impureShellCommands = [
+ # "powershell.exe echo Hello"
+ # ];
+
+ fullName = "M-Labs";
+ organization = "m-labs";
+
+ administratorPassword = "12345";
+
+ # Auto login
+ defaultUser = "artiq";
+
+ # Imperative installation commands, to be installed incrementally
+ installCommands = [
+
+ {
+ name = "Anaconda3";
+ script = let
+ Anaconda3 = pkgs.fetchurl {
+ name = "Anaconda3.exe";
+ url = "https://repo.anaconda.com/archive/Anaconda3-2019.03-Windows-x86_64.exe";
+ sha256 = "1f9icm5rwab6l1f23a70dw0qixzrl62wbglimip82h4zhxlh3jfj";
+ };
+ in ''
+ cp ${Anaconda3} ./Anaconda3.exe
+ win put Anaconda3.exe 'C:\Users\artiq'
+ win exec 'start /wait "" .\Anaconda3.exe /S /D=%UserProfile%\Anaconda3'
+ '';
+ }
+
+ ];
+
+ # services = {
+ # # Enable remote management
+ # WinRm = {
+ # Status = "Running";
+ # PassThru = true;
+ # };
+ # };
+
+ # License key
+ # productKey = "iboughtthisone";
+
+ # Locales
+ # uiLanguage = "en-US";
+ # inputLocale = "en-US";
+ # userLocale = "en-US";
+ # systemLocale = "en-US";
+
+ # packages = [
+ # (
+ # win.pkgs.makeMSIPkg {
+ # # Note: File not in repository, it's meant as an example to subsitute
+ # name = "notepadplusplus";
+ # msi = ./Notepad++7.7.msi;
+ # # Custom cert
+ # # cert = ./notepad++-cert.cer
+ # }
+ # )
+ # (
+ # win.pkgs.makeCrossPkg {
+ # name = "hello";
+ # pkg = pkgs.pkgsCross.mingwW64.hello;
+ # }
+ # )
+ # ];
+
+}
diff --git a/artiq-fast/wfvm/bundle/.envrc b/artiq-fast/wfvm/bundle/.envrc
new file mode 100644
index 0000000..1d953f4
--- /dev/null
+++ b/artiq-fast/wfvm/bundle/.envrc
@@ -0,0 +1 @@
+use nix
diff --git a/artiq-fast/wfvm/bundle/default.nix b/artiq-fast/wfvm/bundle/default.nix
new file mode 100644
index 0000000..a1171ea
--- /dev/null
+++ b/artiq-fast/wfvm/bundle/default.nix
@@ -0,0 +1,9 @@
+{ pkgs ? import {}
+, lib ? pkgs.lib
+}:
+
+pkgs.runCommandNoCC "win-bundle-installer.exe" {} ''
+ cp ${./main.go} main.go
+ env HOME=$(mktemp -d) GOOS=windows GOARCH=amd64 ${pkgs.go}/bin/go build
+ mv build.exe $out
+''
diff --git a/artiq-fast/wfvm/bundle/main.go b/artiq-fast/wfvm/bundle/main.go
new file mode 100644
index 0000000..772bf79
--- /dev/null
+++ b/artiq-fast/wfvm/bundle/main.go
@@ -0,0 +1,116 @@
+package main
+
+import (
+ "archive/tar"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+)
+
+func Untar(dst string, r io.Reader) error {
+
+ tr := tar.NewReader(r)
+
+ for {
+ header, err := tr.Next()
+
+ switch {
+
+ case err == io.EOF:
+ return nil
+ case err != nil:
+ return err
+
+ case header == nil:
+ continue
+ }
+
+ target := filepath.Join(dst, header.Name)
+
+ switch header.Typeflag {
+
+ case tar.TypeDir:
+ if _, err := os.Stat(target); err != nil {
+ if err := os.MkdirAll(target, 0755); err != nil {
+ return err
+ }
+ }
+
+ case tar.TypeReg:
+ f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
+ if err != nil {
+ return err
+ }
+
+ if _, err := io.Copy(f, tr); err != nil {
+ return err
+ }
+
+ f.Close()
+ }
+ }
+}
+
+func InstallBundle(bundlePath string) error {
+
+ reader, err := os.Open(bundlePath)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ workDir, err := ioutil.TempDir("", "bundle_install")
+ if err != nil {
+ return err
+ }
+ defer os.RemoveAll(workDir)
+
+ err = Untar(workDir, reader)
+ if err != nil {
+ return err
+ }
+
+ installScript := filepath.Join(workDir, "install.ps1")
+
+ cmd := exec.Command("powershell", installScript)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ cmd.Dir = workDir
+ err = cmd.Run()
+
+ return err
+}
+
+func main() {
+ // Get path relative to binary
+ baseDir, err := filepath.Abs(filepath.Dir(os.Args[0]))
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ var dirs = [2]string{"bootstrap", "user"}
+
+ for _, pkgDir := range dirs {
+
+ dir := filepath.Join(baseDir, pkgDir)
+
+ files, err := ioutil.ReadDir(dir)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ for _, file := range files {
+ bundle := filepath.Join(dir, file.Name())
+ fmt.Println(fmt.Sprintf("Installing: %s", bundle))
+ err := InstallBundle(bundle)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+
+ }
+
+}
diff --git a/artiq-fast/wfvm/bundle/shell.nix b/artiq-fast/wfvm/bundle/shell.nix
new file mode 100644
index 0000000..20c60ed
--- /dev/null
+++ b/artiq-fast/wfvm/bundle/shell.nix
@@ -0,0 +1,13 @@
+{ pkgs ? import {} }:
+
+pkgs.mkShell {
+
+ buildInputs = [
+ pkgs.go
+ ];
+
+ shellHook = ''
+ unset GOPATH
+ '';
+
+}
diff --git a/artiq-fast/wfvm/default.nix b/artiq-fast/wfvm/default.nix
new file mode 100644
index 0000000..74a82d1
--- /dev/null
+++ b/artiq-fast/wfvm/default.nix
@@ -0,0 +1,7 @@
+{ pkgs ? import {}
+}:
+
+{
+ makeWindowsImage = attrs: import ./win.nix ({ inherit pkgs; } // attrs);
+ pkgs = import ./pkgs.nix { inherit pkgs; };
+}
diff --git a/artiq-fast/wfvm/install.nix b/artiq-fast/wfvm/install.nix
new file mode 100644
index 0000000..9d406f0
--- /dev/null
+++ b/artiq-fast/wfvm/install.nix
@@ -0,0 +1,99 @@
+{ pkgs ? import {}
+, diskImageSize ? "22G"
+, qemuMem ? "4G"
+,
+}:
+
+with pkgs;
+
+let
+ windowsIso = fetchurl {
+ url = "https://software-download.microsoft.com/download/sg/17763.107.101029-1455.rs5_release_svc_refresh_CLIENT_LTSC_EVAL_x64FRE_en-us.iso";
+ sha256 = "668fe1af70c2f7416328aee3a0bb066b12dc6bbd2576f40f812b95741e18bc3a";
+ };
+ anaconda = fetchurl {
+ url = "https://repo.anaconda.com/archive/Anaconda3-2019.03-Windows-x86_64.exe";
+ sha256 = "1f9icm5rwab6l1f23a70dw0qixzrl62wbglimip82h4zhxlh3jfj";
+ };
+
+ escape = builtins.replaceStrings [ "\\" ] [ "\\\\" ];
+ qemu = import ./qemu.nix {
+ inherit pkgs qemuMem;
+ diskImage = "c.img";
+ };
+ # Double-escape because we produce a script from a shell heredoc
+ ssh = cmd: qemu.ssh (escape cmd);
+ scp = qemu.scp;
+
+ sshCondaEnv = cmd: ssh "anaconda\\scripts\\activate && ${cmd}";
+ condaEnv = "artiq-env";
+ condaDepSpecs =
+ builtins.concatStringsSep " "
+ (
+ map (s: "\"${s}\"")
+ (import ../conda-artiq-deps.nix)
+ );
+
+ instructions =
+ builtins.toFile "install.txt"
+ (builtins.readFile ./install.txt);
+in
+stdenv.mkDerivation {
+ name = "windows-installer";
+ src = windowsIso;
+ setSourceRoot = "sourceRoot=`pwd`";
+ unpackCmd = ''
+ ln -s $curSrc windows.iso
+ '';
+ propagatedBuildInputs = qemu.inputs;
+ dontBuild = true;
+ installPhase = ''
+ mkdir -p $out/bin $out/data
+ ln -s $(readlink windows.iso) $out/data/windows.iso
+ cat > $out/bin/windows-installer.sh << EOF
+ #!/usr/bin/env bash
+ set -e -m
+
+ ${qemu.qemu-img} create -f qcow2 c.img ${diskImageSize}
+ ${qemu.runQemu false [] [
+ "-boot"
+ "order=d"
+ "-drive"
+ "file=c.img,index=0,media=disk,cache=unsafe"
+ "-drive"
+ "file=$out/data/windows.iso,index=1,media=cdrom,cache=unsafe"
+ ]} &
+ cat ${instructions}
+ wait
+ EOF
+
+ cat > $out/bin/anaconda-installer.sh << EOF
+ #!/usr/bin/env bash
+ set -e -m
+
+ ${qemu.runQemu false [] [
+ "-boot"
+ "order=c"
+ "-drive"
+ "file=c.img,index=0,media=disk"
+ ]} &
+ sleep 10
+ ${ssh "ver"}
+
+ ${scp anaconda "Anaconda.exe"}
+ ${ssh "start /wait \"\" Anaconda.exe /S /D=%cd%\\anaconda"}
+
+ ${sshCondaEnv "conda config --add channels conda-forge"}
+ ${sshCondaEnv "conda config --add channels m-labs"}
+ ( ${sshCondaEnv "conda update -y conda"} ) || true
+ ${sshCondaEnv "conda update -y --all"}
+ ${sshCondaEnv "conda create -y -n ${condaEnv}"}
+ ${sshCondaEnv "conda install -y -n ${condaEnv} ${condaDepSpecs}"}
+ ${ssh "shutdown /p /f"}
+
+ echo "Waiting for qemu exit"
+ wait
+ EOF
+ chmod a+x $out/bin/*.sh
+ '';
+}
diff --git a/artiq-fast/wfvm/manual-test-run.nix b/artiq-fast/wfvm/manual-test-run.nix
new file mode 100644
index 0000000..a969d8a
--- /dev/null
+++ b/artiq-fast/wfvm/manual-test-run.nix
@@ -0,0 +1,32 @@
+# This runs `run-test.nix` with `nix-build`
+
+{ pkgs ? import {}
+, artiqpkgs ? import ../. { inherit pkgs; }
+, diskImage ? (import ./build.nix { inherit pkgs; })
+, qemuMem ? "2G"
+, testTimeout ? 180
+}:
+
+with pkgs;
+
+let
+ windowsRunner = overrides:
+ import ./run-test.nix (
+ {
+ inherit pkgs diskImage qemuMem testTimeout;
+ sipycoPkg = artiqpkgs.conda-sipyco;
+ artiqPkg = artiqpkgs.conda-artiq;
+ } // overrides
+ );
+in
+
+stdenv.mkDerivation {
+ name = "windows-test";
+
+ phases = [ "installPhase" "checkPhase" ];
+ installPhase = "touch $out";
+ doCheck = true;
+ checkPhase = ''
+ ${windowsRunner { testCommand = "set ARTIQ_ROOT=%cd%\\anaconda\\envs\\artiq-env\\Lib\\site-packages\\artiq\\examples\\kc705_nist_clock&&python -m unittest discover -v artiq.test"; }}/bin/run.sh
+ '';
+}
diff --git a/artiq-fast/wfvm/openssh/README.md b/artiq-fast/wfvm/openssh/README.md
new file mode 100644
index 0000000..78f9817
--- /dev/null
+++ b/artiq-fast/wfvm/openssh/README.md
@@ -0,0 +1 @@
+This file is not publicaly acessible anywhere so had to be extracted from a connected instance
diff --git a/artiq-fast/wfvm/openssh/server-package.cab b/artiq-fast/wfvm/openssh/server-package.cab
new file mode 100644
index 0000000..fd9170c
Binary files /dev/null and b/artiq-fast/wfvm/openssh/server-package.cab differ
diff --git a/artiq-fast/wfvm/pkgs.nix b/artiq-fast/wfvm/pkgs.nix
new file mode 100644
index 0000000..03a71a4
--- /dev/null
+++ b/artiq-fast/wfvm/pkgs.nix
@@ -0,0 +1,110 @@
+{ pkgs ? import {}
+, lib ? pkgs.lib
+}:
+
+/*
+
+This file creates a simple custom simple bundle format containing
+a powershell script plus any required executables and assets.
+
+These are assets that are only handled in the pure build steps.
+
+Impure packages are installed in _another_ step that runs impurely outside of
+the Nix sandbox.
+
+*/
+
+let
+
+ makeBundle =
+ { name
+ , bundle
+ }: pkgs.runCommandNoCC "${name}-archive.tar" {} ''
+ cp -r -L ${bundle} build
+ tar -cpf $out -C build .
+ '';
+
+
+in
+rec {
+
+ /*
+ Make a custom install bundle
+ */
+ makePkg =
+ { name
+ , src
+ , installScript
+ }: let
+ installScript_ = pkgs.writeText "${name}-install-script" installScript;
+
+ bundle = pkgs.runCommandNoCC "${name}-bundle" {} ''
+ mkdir build
+ ln -s ${src} build/"$(stripHash "${src}")"
+ ln -s ${installScript_} build/install.ps1
+ mv build $out
+ '';
+ in
+ makeBundle {
+ inherit name bundle;
+ };
+
+
+ /*
+ Make an install bundle from a .msi
+ */
+ makeMSIPkg =
+ { name
+ , msi
+ , cert ? null
+ , ADDLOCAL ? []
+ , preInstall ? ""
+ , postInstall ? ""
+ }: let
+ installScript = pkgs.writeText "${name}-install-script" ''
+ ${preInstall}
+ ${if cert != null then "certutil.exe -f -addstore TrustedPublisher cert.cer" else ""}
+ msiexec.exe /i .\${name}.msi ${if ADDLOCAL != [] then "ADDLOCAL=" else ""}${lib.concatStringsSep "," ADDLOCAL}
+ ${postInstall}
+ '';
+
+ bundle = pkgs.runCommandNoCC "${name}-bundle" {} ''
+ mkdir build
+ ln -s ${msi} build/${name}.msi
+ ${if cert != null then "ln -s ${cert} build/cert.cer" else ""}
+ ln -s ${installScript} build/install.ps1
+ mv build $out
+ '';
+ in
+ makeBundle {
+ inherit name bundle;
+ };
+
+ /*
+ Nix cross-built packages
+ */
+ makeCrossPkg =
+ { name
+ , pkg
+ , destination ? ''C:\Program Files\${name}\''
+ , preInstall ? ""
+ , postInstall ? ""
+ }: let
+ installScript = pkgs.writeText "${name}-install-script" ''
+ ${preInstall}
+ Copy-Item pkg -Destination "${destination}"
+ ${postInstall}
+ '';
+
+ bundle = pkgs.runCommandNoCC "${name}-bundle" {} ''
+ mkdir -p build/pkg
+ ln -s ${pkg} build/pkg
+ ln -s ${installScript} build/install.ps1
+ mv build $out
+ '';
+ in
+ makeBundle {
+ inherit name bundle;
+ };
+
+}
diff --git a/artiq-fast/wfvm/qemu.nix b/artiq-fast/wfvm/qemu.nix
new file mode 100644
index 0000000..26191f0
--- /dev/null
+++ b/artiq-fast/wfvm/qemu.nix
@@ -0,0 +1,63 @@
+{ pkgs
+, qemuMem
+, sshUser ? "user"
+, sshPassword ? "user"
+,
+}:
+
+with pkgs;
+
+let
+ qemu-img = "${qemu_kvm}/bin/qemu-img";
+ runQemu = isolateNetwork: forwardedPorts: extraArgs:
+ 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:${socat}/bin/socat\\ -\\ tcp:${targetAddr}:${toString port}"
+ ) forwardedPorts
+ );
+ args = [
+ "-enable-kvm"
+ "-m"
+ qemuMem
+ "-bios"
+ "${OVMF.fd}/FV/OVMF.fd"
+ "-netdev"
+ "user,id=n1,net=192.168.1.0/24,restrict=${restrict},hostfwd=tcp::2022-:22${guestfwds}"
+ "-device"
+ "e1000,netdev=n1"
+ ];
+ argStr = builtins.concatStringsSep " " (args ++ extraArgs);
+ in
+ "${qemu_kvm}/bin/qemu-system-x86_64 ${argStr}";
+
+ # Pass empty config file to prevent ssh from failing to create ~/.ssh
+ sshOpts = "-F /dev/null -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=\$TMP/known_hosts";
+ sshWithQuotes = quotes: cmd: ''
+ echo ssh windows ${quotes}${cmd}${quotes}
+ ${sshpass}/bin/sshpass -p${sshPassword} -- \
+ ${openssh}/bin/ssh -np 2022 ${sshOpts} \
+ ${sshUser}@localhost \
+ ${quotes}${cmd}${quotes}
+ '';
+ ssh = sshWithQuotes "'";
+ scp = src: target: ''
+ echo "Copy ${src} to ${target}"
+ ${sshpass}/bin/sshpass -p${sshPassword} -- \
+ ${openssh}/bin/scp -P 2022 ${sshOpts} \
+ "${src}" "${sshUser}@localhost:${target}"
+ '';
+
+in
+{
+ inherit qemu-img runQemu ssh sshWithQuotes scp;
+ inputs = [ qemu_kvm openssh sshpass ];
+}
diff --git a/artiq-fast/wfvm/run-test.nix b/artiq-fast/wfvm/run-test.nix
new file mode 100644
index 0000000..916d571
--- /dev/null
+++ b/artiq-fast/wfvm/run-test.nix
@@ -0,0 +1,91 @@
+{ pkgs
+, sipycoPkg
+, artiqPkg
+, diskImage ? (import ./build.nix { inherit pkgs; })
+, qemuMem ? "2G"
+, testTimeout ? 600
+, testCommand ? "python -m unittest discover -v sipyco.test && python -m unittest discover -v artiq.test"
+,
+}:
+
+with pkgs;
+
+let
+ escape = builtins.replaceStrings [ "\\" ] [ "\\\\" ];
+ qemu = import ./qemu.nix {
+ inherit pkgs qemuMem;
+ };
+ # Double-escape because we produce a script from a shell heredoc
+ ssh = cmd: qemu.ssh (escape cmd);
+ sshUnquoted = qemu.sshWithQuotes "\"";
+ scp = qemu.scp;
+ condaEnv = "artiq-env";
+ tcpPorts = [ 1380 1381 1382 1383 ];
+ forwardedPorts =
+ map (
+ port: {
+ listenAddr = "192.168.1.50";
+ targetAddr = "192.168.1.50";
+ inherit port;
+ }
+ ) tcpPorts;
+in
+
+stdenv.mkDerivation {
+ name = "windows-test-runner";
+
+ # Dummy sources
+ src = pkgs.runCommandNoCC "dummy" {} "touch $out";
+ dontUnpack = true;
+
+ propagatedBuildInputs = qemu.inputs;
+ dontBuild = true;
+ installPhase = ''
+ mkdir -p $out/bin
+ cat > $out/bin/run.sh << EOF
+ #!/usr/bin/env bash
+ set -e -m
+
+ cp ${diskImage} c.img
+
+ ${qemu.runQemu true forwardedPorts [
+ "-boot"
+ "order=c"
+ "-snapshot"
+ "-drive"
+ "file=c.img,index=0,media=disk,cache=unsafe"
+ "-display"
+ "none"
+ ]} &
+
+ echo "Wait for Windows to boot"
+ sleep 30
+ ${ssh "ver"}
+ i=0
+ for pkg in ${sipycoPkg}/noarch/sipyco*.tar.bz2 ${artiqPkg}/noarch/artiq*.tar.bz2 ; do
+ ${scp "\\$pkg" "to_install\\$i.tar.bz2"}
+ ${sshUnquoted "anaconda\\scripts\\activate ${condaEnv} && conda install to_install\\$i.tar.bz2"}
+ ((i=i+1))
+ done
+
+ # Schedule a timed shutdown against hanging test runs
+ ${ssh "shutdown -s -t ${toString testTimeout}"}
+
+ FAIL=n
+ ( ${ssh "anaconda\\scripts\\activate ${condaEnv} && ${testCommand}"} ) || FAIL=y
+
+ # Abort timeouted shutdown
+ ${ssh "shutdown -a"}
+ # Power off immediately
+ ${ssh "shutdown -p -f"}
+ wait
+
+ if [ "\$FAIL" = "y" ]; then
+ exit 1
+ else
+ exit 0
+ fi
+ EOF
+ chmod a+x $out/bin/run.sh
+ '';
+}
diff --git a/artiq-fast/wfvm/win.nix b/artiq-fast/wfvm/win.nix
new file mode 100644
index 0000000..61faa2e
--- /dev/null
+++ b/artiq-fast/wfvm/win.nix
@@ -0,0 +1,264 @@
+{ pkgs ? import {}
+, lib ? pkgs.lib
+, diskImageSize ? "22G"
+, qemuMem ? "4G"
+, windowsImage ? null
+, autoUnattendParams ? {}
+, packages ? []
+, impureMode ? false
+, baseRtc ? "2020-04-20T14:21:42"
+, installCommands ? []
+, users ? {}
+, ...
+}@attrs:
+
+let
+ # qemu_test is a smaller closure only building for a single system arch
+ qemu = pkgs.qemu_test;
+ 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 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.fetchurl {
+ url = "https://software-download.microsoft.com/download/sg/17763.107.101029-1455.rs5_release_svc_refresh_CLIENT_LTSC_EVAL_x64FRE_en-us.iso";
+ sha256 = "668fe1af70c2f7416328aee3a0bb066b12dc6bbd2576f40f812b95741e18bc3a";
+ };
+
+ openSshServerPackage = ./openssh/server-package.cab;
+
+ autounattend = import ./autounattend.nix (
+ attrs // {
+ inherit pkgs;
+ }
+ );
+
+ bundleInstaller = pkgs.callPackage ./bundle {};
+
+ # Packages required to drive installation of other packages
+ bootstrapPkgs = let
+ winPkgs = import ./pkgs.nix { inherit pkgs; };
+
+ in
+ runQemuCommand "bootstrap-win-pkgs.img" ''
+ mkdir pkgs
+ mkdir pkgs/bootstrap
+ mkdir pkgs/user
+ mkdir pkgs/fod
+
+ cp ${bundleInstaller} pkgs/"$(stripHash "${bundleInstaller}")"
+
+ # Install optional windows features
+
+ cp ${openSshServerPackage} pkgs/fod/OpenSSH-Server-Package~31bf3856ad364e35~amd64~~.cab
+
+ # SSH setup script goes here because windows XML parser sucks
+ cp ${autounattend.setupScript} pkgs/ssh-setup.ps1
+
+ ${lib.concatStringsSep "\n" (builtins.map (x: ''cp ${x} pkgs/bootstrap/"$(stripHash "${x}")"'') packages)}
+
+ virt-make-fs --partition --type=fat pkgs/ $out
+ '';
+
+ mkQemuFlags = extraFlags: [
+ "-enable-kvm"
+ "-cpu"
+ "host"
+ "-smp"
+ "$NIX_BUILD_CORES"
+ "-m"
+ "${qemuMem}"
+ "-bios"
+ "${pkgs.OVMF.fd}/FV/OVMF.fd"
+ "-vga"
+ "virtio"
+ "-device"
+ "piix3-usb-uhci" # USB root hub
+ # "CD" drive with windows features-on-demand
+ # "-cdrom" "${fodIso}"
+ # Set the base clock inside the VM
+ "-rtc base=${baseRtc}"
+ # Always enable SSH port forward
+ # It's not really required for the initial setup but we do it here anyway
+ "-netdev user,id=n1,net=192.168.1.0/24,restrict=off,hostfwd=tcp::2022-:22"
+ "-device e1000,netdev=n1"
+ ] ++ lib.optional (!impureMode) "-nographic" ++ extraFlags;
+
+ installScript = pkgs.writeScript "windows-install-script" (
+ let
+ qemuParams = mkQemuFlags [
+ # "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=usbimage.img,if=none,format=raw,readonly=on"
+ "-device"
+ "usb-storage,drive=win-install"
+ # Output image
+ "-drive"
+ "file=c.img,index=0,media=disk,cache=unsafe"
+ ];
+ in
+ ''
+ #!${pkgs.runtimeShell}
+ set -euxo pipefail
+ export PATH=${lib.makeBinPath [ p7zip qemu libguestfs ]}:$PATH
+
+ if test -z "''${NIX_BUILD_CORES+x}"; then
+ export NIX_BUILD_CORES=$(nproc)
+ fi
+
+ # 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
+
+ cp ${autounattend.autounattendXML} win/autounattend.xml
+
+ virt-make-fs --partition --type=fat win/ usbimage.img
+ rm -rf win
+
+ # Qemu requires files to be rw
+ qemu-img create -f qcow2 c.img ${diskImageSize}
+ env NIX_BUILD_CORES="''${NIX_BUILD_CORES:4}" qemu-system-x86_64 ${lib.concatStringsSep " " qemuParams}
+ ''
+ );
+
+ baseImage = pkgs.runCommandNoCC "windows.img" {} ''
+ ${installScript}
+ mv c.img $out
+ '';
+
+ # Use Paramiko instead of OpenSSH
+ #
+ # OpenSSH goes out of it's way to make password logins hard
+ # and Windows goes out of it's way to make key authentication hard
+ # so we're in a pretty tough spot
+ #
+ # Luckily the usage patterns are quite simple and easy to reimplement with paramiko
+ paramikoClient = pkgs.writeScriptBin "win" ''
+ #!${pkgs.python3.withPackages(ps: [ ps.paramiko ])}/bin/python
+ import paramiko
+ import os.path
+ import sys
+
+
+ def w_join(*args):
+ # Like os.path.join but for windows paths
+ return "\\".join(args)
+
+
+ if __name__ == '__main__':
+ client = paramiko.SSHClient()
+ client.set_missing_host_key_policy(paramiko.client.AutoAddPolicy)
+
+
+ cmd = sys.argv[1]
+
+ try:
+ client.connect(hostname="127.0.0.1", port=2022, username="artiq", password="${users.artiq.password}", timeout=1)
+
+ if cmd == "put":
+ sftp = client.open_sftp()
+ src = sys.argv[2]
+ dst = sys.argv[3]
+ sftp.put(src, w_join(dst, os.path.basename(src)))
+
+ elif cmd == "exec":
+ _, stdout, stderr = client.exec_command(sys.argv[2])
+
+ sys.stdout.write(stdout.read().strip().decode())
+ sys.stdout.flush()
+
+ sys.stderr.write(stderr.read().strip().decode())
+ sys.stderr.flush()
+
+ else:
+ raise ValueError(f"Unhandled command: {cmd}")
+ except (EOFError, paramiko.ssh_exception.SSHException):
+ exit(1)
+ '';
+
+ finalImage = builtins.foldl' (acc: v: pkgs.runCommandNoCC "${v.name}.img" {
+ buildInputs = [
+ paramikoClient
+ qemu
+ ];
+ } (let
+ script = pkgs.writeScript "${v.name}-script" v.script;
+ qemuParams = mkQemuFlags [
+ # Output image
+ "-drive"
+ "file=c.img,index=0,media=disk,cache=unsafe"
+ ];
+
+ in ''
+ export HOME=$(mktemp -d)
+
+ # Create an image referencing the previous image in the chain
+ qemu-img create -f qcow2 -b ${acc} c.img
+
+ qemu-system-x86_64 ${lib.concatStringsSep " " qemuParams} &
+
+ # 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 'echo 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 "Executing user script to build layer"
+
+ ${script}
+
+ # Allow install to "settle"
+ sleep 20
+
+ win exec 'shutdown /s'
+
+ mv c.img $out
+ '')) baseImage installCommands;
+
+in
+
+# impureMode is meant for debugging the base image, not the full incremental build process
+if !(impureMode) then finalImage else assert installCommands == []; installScript