From a5d93aea35a5a1058a17e62c28bc867fb289c58c Mon Sep 17 00:00:00 2001 From: adisbladis Date: Thu, 9 Jan 2020 20:19:15 +0100 Subject: [PATCH] windows: Add automated declarative windows install --- artiq-fast/windows/README.md | 27 +-- artiq-fast/windows/autounattend.nix | 295 +++++++++++++++++++++++++ artiq-fast/windows/build.nix | 81 +++++++ artiq-fast/windows/bundle/.envrc | 1 + artiq-fast/windows/bundle/default.nix | 9 + artiq-fast/windows/bundle/main.go | 125 +++++++++++ artiq-fast/windows/bundle/shell.nix | 13 ++ artiq-fast/windows/default.nix | 7 + artiq-fast/windows/install.nix | 28 ++- artiq-fast/windows/manual-test-run.nix | 23 +- artiq-fast/windows/pkgs.nix | 110 +++++++++ artiq-fast/windows/qemu.nix | 37 ++-- artiq-fast/windows/redhat-cert.cer | 28 +++ artiq-fast/windows/run-test.nix | 43 ++-- artiq-fast/windows/win.nix | 153 +++++++++++++ 15 files changed, 911 insertions(+), 69 deletions(-) create mode 100644 artiq-fast/windows/autounattend.nix create mode 100644 artiq-fast/windows/build.nix create mode 100644 artiq-fast/windows/bundle/.envrc create mode 100644 artiq-fast/windows/bundle/default.nix create mode 100644 artiq-fast/windows/bundle/main.go create mode 100644 artiq-fast/windows/bundle/shell.nix create mode 100644 artiq-fast/windows/default.nix create mode 100644 artiq-fast/windows/pkgs.nix create mode 100644 artiq-fast/windows/redhat-cert.cer create mode 100644 artiq-fast/windows/win.nix diff --git a/artiq-fast/windows/README.md b/artiq-fast/windows/README.md index 279e967..07962a7 100644 --- a/artiq-fast/windows/README.md +++ b/artiq-fast/windows/README.md @@ -2,24 +2,19 @@ ## Install a Windows image +1. Adjust build.nix accordingly +2. Run: + +If in impure mode ```shell -nix-build install.nix -I artiqSrc=…/artiq -result/bin/windows-installer.sh +nix-build build.nix +./result ``` +Results in a file called c.img -Follow the instructions. - -## Install Anaconda to the image - +If in pure mode ```shell -result/bin/anaconda-installer.sh -``` - -Move the image `c.img` to one of Nix' `extra-sandbox-paths` (`nix.sandboxPaths` on NixOS). - - -# Running the tests manually - -```shell -nix-build --pure --arg diskImage "\"…/c.img\"" -I artiqSrc=…/artiq manual-test-run.nix +nix-build build.nix +ls -la ./result ``` +Results in a symlink to the image in the nix store diff --git a/artiq-fast/windows/autounattend.nix b/artiq-fast/windows/autounattend.nix new file mode 100644 index 0000000..c72b3ae --- /dev/null +++ b/artiq-fast/windows/autounattend.nix @@ -0,0 +1,295 @@ +{ 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 ? {} +, impureMode ? false +, impureShellCommands ? [] +, ... +}: + +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; + + # If we are running in impure mode we can also enable networked services + impureSetupCommands = let + userSSHKeys = lib.flatten (lib.mapAttrsToList (n: v: v.sshKeys or []) users); + keyCommands = ( + builtins.foldl' ( + acc: key: acc ++ [ + ''"${key}" | Out-File C:\usersshkey.pub'' + "ssh-add C:\usersshkey.pub" + ] + ) [] userSSHKeys + ) ++ [ "Remove-Item C:\usersshkey.pub" ]; + + in + if impureMode then [ + { + Path = "powershell.exe Install-Module -Force OpenSSHUtils -Scope AllUsers"; + Description = "Install Openssh."; + } + { + Path = "powershell.exe Start-Service ssh-agent"; + Description = "Start the ssh-agent"; + } + { + Path = "powershell.exe Start-Service sshd"; + Description = "Now start the sshd service"; + } + ] ++ keyCommands else []; + + 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."; + } + ] + ++ setupCommands + # ++ impureSetupCommands + ++ serviceCommands + ++ impureShellCommands + ++ [ + { + Path = ''powershell.exe F:\win-bundle-installer.exe''; + Description = "Install any declared packages."; + } + ] + ); + + 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); + + 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> + </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>EFI</Type> + <Size>100</Size> + </CreatePartition> + <CreatePartition wcm:action="add"> + <Order>2</Order> + <Type>MSR</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>FAT32</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>0</DiskID> + <WillWipeDisk>true</WillWipeDisk> + </Disk> + </DiskConfiguration> + + <ImageInstall> + <OSImage> + <InstallTo> + <DiskID>0</DiskID> + <PartitionID>3</PartitionID> + </InstallTo> + <InstallFrom> + <MetaData wcm:action="add"> + <Key>/IMAGE/INDEX</Key> + <Value>1</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> + </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#Windows 10 Enterprise LTSC 2019 Evaluation" xmlns:cpi="urn:schemas-microsoft-com:cpi" /> + </unattend> + ''; + +in + # Lint and format as a sanity check +pkgs.runCommandNoCC "autounattend.xml" {} '' + ${pkgs.libxml2}/bin/xmllint --format ${autounattendXML} > $out +'' diff --git a/artiq-fast/windows/build.nix b/artiq-fast/windows/build.nix new file mode 100644 index 0000000..28f93a8 --- /dev/null +++ b/artiq-fast/windows/build.nix @@ -0,0 +1,81 @@ +{ pkgs ? import <nixpkgs> {} }: + +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 = { + # sshKeys = [ + # "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEmJW3Z+1ZNNVao2jcipQQxiEN27jtpl40fq3Je+jgir" + # ]; + password = "1234"; + # description = "Default user"; + # displayName = "Display name"; + groups = [ + "Administrators" + ]; + }; + }; + + # Will also enable ssh + # These impure commands need sandbox disabled or run outside of the sandbox + impureMode = true; + + # impureShellCommands = [ + # "powershell.exe echo Hello" + # ]; + + fullName = "M-Labs"; + organization = "m-labs"; + + administratorPassword = "12345"; + + # Auto login + defaultUser = "artiq"; + + # 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/windows/bundle/.envrc b/artiq-fast/windows/bundle/.envrc new file mode 100644 index 0000000..1d953f4 --- /dev/null +++ b/artiq-fast/windows/bundle/.envrc @@ -0,0 +1 @@ +use nix diff --git a/artiq-fast/windows/bundle/default.nix b/artiq-fast/windows/bundle/default.nix new file mode 100644 index 0000000..a1171ea --- /dev/null +++ b/artiq-fast/windows/bundle/default.nix @@ -0,0 +1,9 @@ +{ pkgs ? import <nixpkgs> {} +, 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/windows/bundle/main.go b/artiq-fast/windows/bundle/main.go new file mode 100644 index 0000000..a69e544 --- /dev/null +++ b/artiq-fast/windows/bundle/main.go @@ -0,0 +1,125 @@ +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 Shutdown() { + if err := exec.Command("cmd", "/C", "shutdown", "/s", "/f", "/t", "00").Run(); err != nil { + fmt.Println("Failed to initiate shutdown:", err) + } +} + +func main() { + + defer Shutdown() + + // 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/windows/bundle/shell.nix b/artiq-fast/windows/bundle/shell.nix new file mode 100644 index 0000000..20c60ed --- /dev/null +++ b/artiq-fast/windows/bundle/shell.nix @@ -0,0 +1,13 @@ +{ pkgs ? import <nixpkgs> {} }: + +pkgs.mkShell { + + buildInputs = [ + pkgs.go + ]; + + shellHook = '' + unset GOPATH + ''; + +} diff --git a/artiq-fast/windows/default.nix b/artiq-fast/windows/default.nix new file mode 100644 index 0000000..74a82d1 --- /dev/null +++ b/artiq-fast/windows/default.nix @@ -0,0 +1,7 @@ +{ pkgs ? import <nixpkgs> {} +}: + +{ + makeWindowsImage = attrs: import ./win.nix ({ inherit pkgs; } // attrs); + pkgs = import ./pkgs.nix { inherit pkgs; }; +} diff --git a/artiq-fast/windows/install.nix b/artiq-fast/windows/install.nix index 9ac5e7a..85cc244 100644 --- a/artiq-fast/windows/install.nix +++ b/artiq-fast/windows/install.nix @@ -1,6 +1,7 @@ -{ pkgs ? import <nixpkgs> {}, - diskImageSize ? "22G", - qemuMem ? "4G", +{ pkgs ? import <nixpkgs> {} +, diskImageSize ? "22G" +, qemuMem ? "4G" +, }: with pkgs; @@ -33,7 +34,7 @@ let instructions = builtins.toFile "install.txt" - (builtins.readFile ./install.txt); + (builtins.readFile ./install.txt); in stdenv.mkDerivation { name = "windows-installer"; @@ -53,10 +54,13 @@ stdenv.mkDerivation { ${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" - ]} & + "-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 @@ -66,9 +70,11 @@ stdenv.mkDerivation { set -e -m ${qemu.runQemu false [] [ - "-boot" "order=c" - "-drive" "file=c.img,index=0,media=disk" - ]} & + "-boot" + "order=c" + "-drive" + "file=c.img,index=0,media=disk" + ]} & sleep 10 ${ssh "ver"} diff --git a/artiq-fast/windows/manual-test-run.nix b/artiq-fast/windows/manual-test-run.nix index b04ae8d..fc90cc6 100644 --- a/artiq-fast/windows/manual-test-run.nix +++ b/artiq-fast/windows/manual-test-run.nix @@ -1,21 +1,24 @@ # This runs `run-test.nix` with `nix-build` -{ pkgs ? import <nixpkgs> {}, - artiqpkgs ? import ../. { inherit pkgs; }, - diskImage ? "/opt/windows/c.img", - qemuMem ? "2G", - testTimeout ? 180, +{ pkgs ? import <nixpkgs> {} +, artiqpkgs ? import ../. { inherit pkgs; } +, diskImage ? "/opt/windows/c.img" +, 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); + import ./run-test.nix ( + { + inherit pkgs diskImage qemuMem testTimeout; + sipycoPkg = artiqpkgs.conda-sipyco; + artiqPkg = artiqpkgs.conda-artiq; + } // overrides + ); in stdenv.mkDerivation { diff --git a/artiq-fast/windows/pkgs.nix b/artiq-fast/windows/pkgs.nix new file mode 100644 index 0000000..03a71a4 --- /dev/null +++ b/artiq-fast/windows/pkgs.nix @@ -0,0 +1,110 @@ +{ pkgs ? import <nixpkgs> {} +, 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/windows/qemu.nix b/artiq-fast/windows/qemu.nix index e575f0d..fc26017 100644 --- a/artiq-fast/windows/qemu.nix +++ b/artiq-fast/windows/qemu.nix @@ -1,8 +1,9 @@ -{ pkgs, - diskImage, - qemuMem, - sshUser ? "user", - sshPassword ? "user", +{ pkgs +, diskImage +, qemuMem +, sshUser ? "user" +, sshPassword ? "user" +, }: with pkgs; @@ -18,18 +19,26 @@ let # 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); + ( + 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" + "-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}"; + 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"; @@ -47,7 +56,7 @@ let ${openssh}/bin/scp -P 2022 ${sshOpts} \ "${src}" "${sshUser}@localhost:${target}" ''; - + in { inherit qemu-img runQemu ssh sshWithQuotes scp; diff --git a/artiq-fast/windows/redhat-cert.cer b/artiq-fast/windows/redhat-cert.cer new file mode 100644 index 0000000..7fb3357 --- /dev/null +++ b/artiq-fast/windows/redhat-cert.cer @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIE1jCCA76gAwIBAgIQXRDLGOs6eQCHg6t0d/nTGTANBgkqhkiG9w0BAQsFADCB +hDELMAkGA1UEBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8w +HQYDVQQLExZTeW1hbnRlYyBUcnVzdCBOZXR3b3JrMTUwMwYDVQQDEyxTeW1hbnRl +YyBDbGFzcyAzIFNIQTI1NiBDb2RlIFNpZ25pbmcgQ0EgLSBHMjAeFw0xODExMjcw +MDAwMDBaFw0yMjAxMjUyMzU5NTlaMGgxCzAJBgNVBAYTAlVTMRcwFQYDVQQIDA5O +b3J0aCBDYXJvbGluYTEQMA4GA1UEBwwHUmFsZWlnaDEWMBQGA1UECgwNUmVkIEhh +dCwgSW5jLjEWMBQGA1UEAwwNUmVkIEhhdCwgSW5jLjCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAN6tLWiLXZXnYDRc6y9qeQrnN59qP5xutjQ4AHZY/m9E +aNMRzKOONgalW6YTQRrW6emIscqlweRzvDnrF4hv/u/SfIq16XLqdViL0tZjmFWY +hijbtFP1cjEZNeS47m2YnQgTpTsKmZ5A66/oiqzg8ogNbxxilUOojQ+rjzhwsvfJ +AgnaGhOMeR81ca2YsgzFX3Ywf7iy6A/CtjHIOh78wcwR0MaJW6QvOhOaClVhHGtq +8yIUA7k/3k8sCC4xIxci2UqFOXopw0EUvd/xnc5by8m7LYdDO048sOM0lASt2d4P +KniOvUkU/LpqiFSYo/6272j+KRBDYCW2IgPCK5HWlZMCAwEAAaOCAV0wggFZMAkG +A1UdEwQCMAAwDgYDVR0PAQH/BAQDAgeAMCsGA1UdHwQkMCIwIKAeoByGGmh0dHA6 +Ly9yYi5zeW1jYi5jb20vcmIuY3JsMGEGA1UdIARaMFgwVgYGZ4EMAQQBMEwwIwYI +KwYBBQUHAgEWF2h0dHBzOi8vZC5zeW1jYi5jb20vY3BzMCUGCCsGAQUFBwICMBkM +F2h0dHBzOi8vZC5zeW1jYi5jb20vcnBhMBMGA1UdJQQMMAoGCCsGAQUFBwMDMFcG +CCsGAQUFBwEBBEswSTAfBggrBgEFBQcwAYYTaHR0cDovL3JiLnN5bWNkLmNvbTAm +BggrBgEFBQcwAoYaaHR0cDovL3JiLnN5bWNiLmNvbS9yYi5jcnQwHwYDVR0jBBgw +FoAU1MAGIknrOUvdk+JcobhHdglyA1gwHQYDVR0OBBYEFG9GZUQmGAU3flEwvkNB +0Dhx23xpMA0GCSqGSIb3DQEBCwUAA4IBAQBX36ARUohDOhdV52T3imb+YRVdlm4k +9eX4mtE/Z+3vTuQGeCKgRFo10w94gQrRCRCQdfeyRsJHSvYFbgdGf+NboOxX2MDQ +F9ARGw6DmIezVvNJCnngv19ULo1VrDDH9tySafmb1PFjkYwcl8a/i2MWQqM/erne +y9aHFHGiWiGfWu8GWc1fmnZdG0LjlzLWn+zvYKmRE30v/Hb8rRhXpEAUUvaB4tNo +8ahQCl00nEBsr7tNKLabf9OfxXLp3oiMRfzWLBG4TavH5gWS5MgXBiP6Wxidf93v +MkM3kaYRRj+33lHdchapyKtWzgvhHa8kjDBB5oOXYhc08zqbfMpf9vNm +-----END CERTIFICATE----- diff --git a/artiq-fast/windows/run-test.nix b/artiq-fast/windows/run-test.nix index 6293d2e..46932e1 100644 --- a/artiq-fast/windows/run-test.nix +++ b/artiq-fast/windows/run-test.nix @@ -1,10 +1,11 @@ -{ pkgs, - sipycoPkg, - artiqPkg, - diskImage ? "/opt/windows/c.img", - qemuMem ? "2G", - testTimeout ? 600, - testCommand ? "python -m unittest discover -v sipyco.test && python -m unittest discover -v artiq.test", +{ pkgs +, sipycoPkg +, artiqPkg +, diskImage ? "/opt/windows/c.img" +, qemuMem ? "2G" +, testTimeout ? 600 +, testCommand ? "python -m unittest discover -v sipyco.test && python -m unittest discover -v artiq.test" +, }: with pkgs; @@ -22,11 +23,13 @@ let condaEnv = "artiq-env"; tcpPorts = [ 1380 1381 1382 1383 ]; forwardedPorts = - map (port: { - listenAddr = "192.168.1.50"; - targetAddr = "192.168.1.50"; - inherit port; - }) tcpPorts; + map ( + port: { + listenAddr = "192.168.1.50"; + targetAddr = "192.168.1.50"; + inherit port; + } + ) tcpPorts; in stdenv.mkDerivation { @@ -44,12 +47,16 @@ stdenv.mkDerivation { # +1 day from last modification of the disk image CLOCK=$(date -Is -d @$(expr $(stat -c %Y ${diskImage}) + 86400)) ${qemu.runQemu true forwardedPorts [ - "-boot" "order=c" - "-snapshot" - "-drive" "file=${diskImage},index=0,media=disk,cache=unsafe" - "-rtc" "base=\\$CLOCK" - "-display" "none" - ]} & + "-boot" + "order=c" + "-snapshot" + "-drive" + "file=${diskImage},index=0,media=disk,cache=unsafe" + "-rtc" + "base=\\$CLOCK" + "-display" + "none" + ]} & echo "Wait for Windows to boot" sleep 30 diff --git a/artiq-fast/windows/win.nix b/artiq-fast/windows/win.nix new file mode 100644 index 0000000..d5ff4cc --- /dev/null +++ b/artiq-fast/windows/win.nix @@ -0,0 +1,153 @@ +{ pkgs ? import <nixpkgs> {} +, lib ? pkgs.lib +, diskImageSize ? "22G" +, qemuMem ? "4G" +, windowsImage ? null +, autoUnattendParams ? {} +, packages ? [] +, impureMode ? false +, ... +}@attrs: + +let + # qemu_test is a smaller closure only building for a single system arch + qemu = pkgs.qemu_test; + libguestfs = pkgs.libguestfs-with-appliance.override { + inherit qemu; + }; + + runQemuCommand = name: command: ( + pkgs.runCommandNoCC name { buildInputs = [ pkgs.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"; + }; + + 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; }; + + # Get updates from https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/ + virtioWin = winPkgs.makeMSIPkg { + name = "virtio-win"; + cert = ./redhat-cert.cer; + msi = pkgs.fetchurl { + url = "https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.173-2/virtio-win-gt-x64.msi"; + sha256 = "0gmxw45fh22kxil1h2d42gxsri9diqfl7rdsxw2r261vvxrmrlq2"; + }; + ADDLOCAL = [ + "FE_balloon_driver" + "FE_network_driver" + "FE_pvpanic_driver" + "FE_qemufwcfg_driver" + "FE_qemupciserial_driver" + "FE_qxl_driver" + "FE_spice_driver" + "FE_viorng_driver" + "FE_vioscsi_driver" + "FE_vioserial_driver" + "FE_viostor_driver" + ]; + }; + + autohotkey = winPkgs.makePkg { + name = "autohotkey"; + src = pkgs.fetchurl { + url = "https://www.autohotkey.com/download/1.0/AutoHotkey104805_Install.exe"; + sha256 = "1f87w9g7f7dr0fq212vmg3zmabxw2013cf2i85zxdllyqbkw64a3"; + }; + installScript = '' + .\AutoHotkey104805_Install.exe /S + ''; + }; + + in + runQemuCommand "bootstrap-win-pkgs.img" '' + mkdir pkgs + mkdir pkgs/bootstrap + mkdir pkgs/user + + cp ${autohotkey} pkgs/bootstrap/"$(stripHash "${autohotkey}")" + cp ${bundleInstaller} pkgs/"$(stripHash "${bundleInstaller}")" + + ${lib.concatStringsSep "\n" (builtins.map (x: ''cp ${x} pkgs/bootstrap/"$(stripHash "${x}")"'') packages)} + + virt-make-fs --partition --type=fat pkgs/ $out + ''; + + installScript = pkgs.writeScript "windows-install-script" ( + let + qemuParams = [ + "-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 + # 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" + # "CD" drive with bootstrap pkgs + "-drive" + "id=virtio-win,file=${bootstrapPkgs},if=none,format=raw,readonly=on" + "-device" + "usb-storage,drive=virtio-win" + ] ++ lib.optional (!impureMode) "-nographic"; + in + '' + #!${pkgs.runtimeShell} + set -exuo pipefail + export PATH=${lib.makeBinPath [ pkgs.p7zip qemu libguestfs ]}:$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 + cp ${autounattend} 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} + '' + ); + +in +if impureMode then installScript else pkgs.runCommandNoCC "windows.img" {} '' + ${installScript} + mv c.img $out +''