diff --git a/tools/devboxes/README.md b/tools/devboxes/README.md new file mode 100644 index 00000000000..e340ba7a397 --- /dev/null +++ b/tools/devboxes/README.md @@ -0,0 +1,32 @@ +# Devboxes + +This directory contains tools to create virtual machine images to help you test workerd on different machines/OSes. + +Right now there is only a Windows devbox. + +## Pre-requisites + +This directory has been tested with: + +- QEMU 6.2.0 +- libvirt 8.0.0 +- aria2c 1.36.0 +- Packer 1.11.2 + +### Ubuntu 22.04 + +On Ubuntu 22.04, [install Packer manually](https://developer.hashicorp.com/packer/install). The version in APT is too old. + +The rest you can install from APT: + +```sh +sudo apt-get install qemu-system-x86 libvirt-daemon-system aria2 +``` + +You'll need to add yourself to the `kvm` and `libvirt` groups in order to use QEMU with KVM acceleration, and to communicate with libvirtd. + +```sh +sudo adduser $(whoami) kvm +sudo adduser $(whoami) libvirt +# Remember to log out and in again for the groups to take effect! +``` diff --git a/tools/devboxes/windows-2022/.gitignore b/tools/devboxes/windows-2022/.gitignore new file mode 100644 index 00000000000..2829e042f06 --- /dev/null +++ b/tools/devboxes/windows-2022/.gitignore @@ -0,0 +1,2 @@ +deps/ +deps.auto.pkrvars.hcl diff --git a/tools/devboxes/windows-2022/README.md b/tools/devboxes/windows-2022/README.md new file mode 100644 index 00000000000..ece929d26c0 --- /dev/null +++ b/tools/devboxes/windows-2022/README.md @@ -0,0 +1,165 @@ +# Windows Server 2022 development box + +This directory contains installation and provisioning Packer templates for Windows Server 2022 Desktop. + +Note: Up until recently, Microsoft used to publish "dev VMs". As of Octoboer 23, 2024, they are unavailable ["due to ongoing technical issues"](https://developer.microsoft.com/en-us/windows/downloads/virtual-machines/). If they ever come back, we could use them instead of this manually-built VM image. + +Note: [Winget does not currently work on Windows Server 2022 Core](https://github.com/microsoft/winget-cli/issues/4319), which is why we use the Desktop Experience instead. + +This directory is split into two separate Packer templates: +- `windows-2022/base` (installation, based on https://github.com/StefanScherer/packer-windows) +- `windows-2022` (provisioning, this directory) + +The reason for this split into two Packer builds is to make it easier to hack on the provisioning scripts without having to wait for a full re-installation. The `windows-2022` template is configured with a copy-on-write image using the `windows-2022/base` template's read-only image as a backing file, so the `windows-2022` image can be relatively quickly recreated. + +Steps to build and use this devbox: +1. Install pre-requisites +1. Fetch dependencies +1. Configure custom root CA (if applicable) +1. Run `packer build` +1. Import the resulting .qcow2 file into a VM with `virt-manager` +1. Make an SSH config on host OS +1. Configure Git on guest OS +1. (Optional) Set up a shared drive + +## Install pre-requisites + +As described in the parent directory's [README.md](../README.md). + +## Fetch dependencies + +There's a script in this directory, `fetch-deps.sh`, which uses `aria2c` to download some binary dependencies for the Packer builds. You can run it from any directory, and the dependencies will end up in this directory (and the `base/` subdirectory). + +```sh +./fetch-deps.sh +``` + +The script also creates a couple of `deps.auto.pkrvars.hcl` files, which tell our Packer templates the absolute paths to the dependency directories. Without these files, our Packer builds will produce errors nudging you to run `fetch-deps.sh`. + +## Configure custom root CA (if applicable) + +If you need to use a custom root CA (e.g., you use Cloudflare One / Warp), create a file in this directory like so: + +``` +# custom-root-ca.auto.pkrvars.hcl +custom_root_ca = "/path/to/my/ca.pem" +``` + +If you do not need to use a custom root CA, you will still need to configure this variable. Set it to the empty string to indicate no custom root CA is required. + +``` +# custom-root-ca.auto.pkrvars.hcl +custom_root_ca = "" # intentionally empty +``` + +The reason this variable is required is to make it harder to accidentally forget to embed a custom root CA if you do need one, which leads to lots of follow-on troubleshooting. + +## Run `packer build` + +Run `packer build` against first this directory's `base/` subdirectory, then this directory itself. + +```sh +packer build ./base +packer build . +``` + +This will produce two output directories in your current working directory, named `output-windows-2022-base/` and `output-windows-2022/`. Each of these directories has a QCOW2 image inside it, named `windows-2022-base.qcow2` and `windows-2022.qcow2`. The first image is a read-only snapshot of a fresh installation of Windows. The second image is a copy-on-write overlay using the first as a backing file, and contains Windows plus various utilities provisioned: your custom root CA, virtiofs, Winget, SSH, Git, MSYS, etc. + +Changing the first, base image (`windows-2022-base.qcow2`) in any way will corrupt the overlay (`windows-2022.qcow2`). If you would like a standalone QCOW2 image, you can use `qemu-img` to convert it. This takes a while, which is why the Packer template doesn't do this for you. + +## Import the resulting .qcow2 file into a VM with `virt-manager` + +Run `virt-manager`, and create a new virtual machine, importing `output-windows-2022/windows-2022.qcow2`. Give the machine plenty of RAM and CPU cores (I use 16G RAM and 16 cores). + +You can now develop directly inside of the Windows desktop via virt-manager's VM console, if you wish. + +## Make an SSH config on host OS + +Once your VM boots, take note of its IP address so you can SSH into it. You can find the VM's IP address in `virt-manager` by looking at the VM's info page and selecting its NIC after it has started up for the first time. + +Add the following to your `~/.ssh/config`: + +``` +Host winbox + User vagrant + HostName +``` + +You should now be able to use VSCode's Remote-SSH extension running on your host OS to connect to the `winbox` SSH host, and develop remotely. + +Note that if you ever delete and recreate the VM, it will likely be assigned a new IP address. + +## Configure Git on guest OS + +Remember to configure your contact information: + +```sh +git config --global user.name "My Name" +git config --global user.email "my@example.com" +``` + +You'll also need some way of authenticating with GitHub. One easy way is to generate a (classic) GitHub Personal Access Token with access to `repos`. First, configure a simple credential store, like DPAPI, on the guest OS: + +```sh +git config --global credential.credentialStore dpapi +``` + +Then create your access token in the GitHub developer settings page, and add it like so: + +```sh +git credential approve +# Enter the following on stdin: +protocol=https +host=github.com +username=your_github_username +password=your_github_personal_access_token +# Press enter twice +``` + +Now you can push from your Windows guest OS to repos you have write access to. + +## Set up a shared drive + +To configure a shared drive for your VM, first shut the VM down if it is already running. + +Next, go to the VM's info page in `virt-manager`, click on the "Memory" panel, check "Enable shared memory", and click "Apply". + +Lastly, from the VM's info page, click "Add Hardware" -> "Filesystem" -> ensure "Driver is "virtiofs", and fill in the "Source path" to point to your desired host-side shared directory. Put whatever identifier you want in the "Target path" textbox, it doesn't matter. + +The next time you start the VM, you should see your shared directory available as drive Z:\. + +# Options + +## Graphical output, headless mode + +By default, the build displays the QEMU console, which shows the VM's graphical output. This allows you to observe the Windows installation process directly. + +To inhibit this behavior, add `--var headless=true` to your `packer build` command. + +# Troubleshooting + +## Observability tools + +Your two basic tools for gaining visibility onto what is going on in the Packer build is setting the `PACKER_LOG=1` environment variable, and viewing the VM's graphical output, via the QEMU console (`--var headless=false`). (It's also possible to connect via VNC, even in headless mode.) + +## `qemu: Waiting for WinRM to become available...` hangs forever + +Check the QEMU console to see what the VM's graphical output tells you. + +## Packer fails with a qemu-img error "Backing file specified without backing format" + +You are using QEMU version >= 6.1 and Packer QEMU Plugin < 1.1.0. Upgrade your plugin with `packer init`. + +## QEMU fails with" /usr/bin/qemu-system-x86_64: symbol lookup error: /snap/core20/current/lib/x86_64-linux-gnu/libpthread.so.0: undefined symbol: __libc_pthread_init, version GLIBC_PRIVATE" + +You are probably running Packer inside a VS Code terminal. Try running Packer in a regular terminal. + +I don't know the exact cause of this failure, but it affects multiple programs, not just QEMU. + +## Windows shows a BSOD during boot with stop code 0x0000007b / INACCESSIBLE_BOOT_DEVICE. + +This can be caused if you change the size of the hard drive after installation. In particular, I encountered it when `windows-2022-base`'s Packer template used a different hard disk size than `windows-2022`'s template. + +It may be possible to set up a post-login script which hacks around this issue, as described here: https://superuser.com/a/1439626 + +There can be many other causes of this BSOD as well, unfortunately. diff --git a/tools/devboxes/windows-2022/base/answer_files/2022/Autounattend.xml b/tools/devboxes/windows-2022/base/answer_files/2022/Autounattend.xml new file mode 100644 index 00000000000..30d100c6a8d --- /dev/null +++ b/tools/devboxes/windows-2022/base/answer_files/2022/Autounattend.xml @@ -0,0 +1,312 @@ + + + + + + + + + E:\viostor\2k22\amd64 + + + + E:\NetKVM\2k22\amd64 + + + + E:\Balloon\2k22\amd64 + + + + E:\pvpanic\2k22\amd64 + + + + E:\qemupciserial\2k22\amd64 + + + + + + + E:\vioinput\2k22\amd64 + + + + E:\viorng\2k22\amd64 + + + + E:\vioscsi\2k22\amd64 + + + + E:\vioserial\2k22\amd64 + + + + + + + en-US + + en-US + en-US + en-US + en-US + en-US + + + + + + + Primary + 1 + 350 + + + 2 + Primary + true + + + + + true + NTFS + + 1 + 1 + + + NTFS + + C + 2 + 2 + + + 0 + true + + + + + + + /IMAGE/NAME + + Windows Server 2022 SERVERDATACENTER + + + + 0 + 2 + + + + + + + + + + OnError + + true + Vagrant + Vagrant + + + + + + + false + + vagrant-2022 + + Pacific Standard Time + + + + true + + + false + false + + + true + + + true + + + + + 1 + Set Execution Policy 64 Bit + cmd.exe /c powershell -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force" + + + 2 + Set Execution Policy 32 Bit + cmd.exe /c powershell -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force" + + + 3 + Disable WinRM + C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File a:\disable-winrm.ps1 + + + + + + + + + vagrant + true</PlainText> + </Password> + <Enabled>true</Enabled> + <Username>vagrant</Username> + </AutoLogon> + <FirstLogonCommands> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c powershell -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force"</CommandLine> + <Description>Set Execution Policy 64 Bit</Description> + <Order>1</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>C:\Windows\SysWOW64\cmd.exe /c powershell -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force"</CommandLine> + <Description>Set Execution Policy 32 Bit</Description> + <Order>2</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File a:\disable-winrm.ps1</CommandLine> + <Description>Disable WinRM</Description> + <Order>3</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\System32\reg.exe ADD HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\ /v HideFileExt /t REG_DWORD /d 0 /f</CommandLine> + <Order>4</Order> + <Description>Show file extensions in Explorer</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\System32\reg.exe ADD HKCU\Console /v QuickEdit /t REG_DWORD /d 1 /f</CommandLine> + <Order>5</Order> + <Description>Enable QuickEdit mode</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\System32\reg.exe ADD HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\ /v Start_ShowRun /t REG_DWORD /d 1 /f</CommandLine> + <Order>6</Order> + <Description>Show Run command in Start Menu</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\System32\reg.exe ADD HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\ /v StartMenuAdminTools /t REG_DWORD /d 1 /f</CommandLine> + <Order>7</Order> + <Description>Show Administrative Tools in Start Menu</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\System32\reg.exe ADD HKLM\SYSTEM\CurrentControlSet\Control\Power\ /v HibernateFileSizePercent /t REG_DWORD /d 0 /f</CommandLine> + <Order>8</Order> + <Description>Zero Hibernation File</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\System32\reg.exe ADD HKLM\SYSTEM\CurrentControlSet\Control\Power\ /v HibernateEnabled /t REG_DWORD /d 0 /f</CommandLine> + <Order>9</Order> + <Description>Disable Hibernation Mode</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c wmic useraccount where "name='vagrant'" set PasswordExpires=FALSE</CommandLine> + <Order>10</Order> + <Description>Disable password expiration for vagrant user</Description> + </SynchronousCommand> + <!-- TODO: Can this be moved to a Packer provisioner stage? It seems to hang when run there. --> + <SynchronousCommand wcm:action="add"> + <CommandLine>E:\virtio-win-guest-tools.exe /passive /norestart</CommandLine> + <Order>11</Order> + <Description>Install VirtIO drivers</Description> + </SynchronousCommand> + <!-- WITHOUT WINDOWS UPDATES --> + + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File a:\enable-winrm.ps1</CommandLine> + <Description>Enable WinRM</Description> + <Order>99</Order> + </SynchronousCommand> + + <!-- END WITHOUT WINDOWS UPDATES --> + <!-- WITH WINDOWS UPDATES --> + <!-- <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c a:\microsoft-updates.bat</CommandLine> + <Order>98</Order> + <Description>Enable Microsoft Updates</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File a:\disable-screensaver.ps1</CommandLine> + <Description>Disable Screensaver</Description> + <Order>99</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File a:\win-updates.ps1</CommandLine> + <Description>Install Windows Updates</Description> + <Order>100</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> --> + <!-- END WITH WINDOWS UPDATES --> + </FirstLogonCommands> + <OOBE> + <HideEULAPage>true</HideEULAPage> + <HideLocalAccountScreen>true</HideLocalAccountScreen> + <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen> + <HideOnlineAccountScreens>true</HideOnlineAccountScreens> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <NetworkLocation>Home</NetworkLocation> + <ProtectYourPC>1</ProtectYourPC> + </OOBE> + <UserAccounts> + <AdministratorPassword> + <Value>vagrant</Value> + <PlainText>true</PlainText> + </AdministratorPassword> + <LocalAccounts> + <LocalAccount wcm:action="add"> + <Password> + <Value>vagrant</Value> + <PlainText>true</PlainText> + </Password> + <Group>administrators</Group> + <DisplayName>Vagrant</DisplayName> + <Name>vagrant</Name> + <Description>Vagrant User</Description> + </LocalAccount> + </LocalAccounts> + </UserAccounts> + <RegisteredOwner /> + </component> + </settings> + <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> + <!-- TODO: Change to Server 2022? --> + <cpi:offlineImage cpi:source="wim:c:/wim/install.wim#Windows Server 2012 R2 SERVERSTANDARD" xmlns:cpi="urn:schemas-microsoft-com:cpi" /> +</unattend> diff --git a/tools/devboxes/windows-2022/base/scripts/dis-updates.bat b/tools/devboxes/windows-2022/base/scripts/dis-updates.bat new file mode 100644 index 00000000000..3ff0c8f34a9 --- /dev/null +++ b/tools/devboxes/windows-2022/base/scripts/dis-updates.bat @@ -0,0 +1,20 @@ +rem http://www.windows-commandline.com/disable-automatic-updates-command-line/ +reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update" /v AUOptions /t REG_DWORD /d 1 /f + +rem remove optional WSUS server settings +reg delete "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /f + +rem even harder, disable windows update service +rem sc config wuauserv start= disabled +rem net stop wuauserv +set logfile=C:\Windows\Temp\win-updates.log + +if exist %logfile% ( + echo Show Windows Updates log file %logfile% + dir %logfile% + type %logfile% + rem output of type command is not fully shown in packer/ssh session, so try PowerShell + rem but it will hang if log file is about 22 KByte + rem powershell -command "Get-Content %logfile%" + echo End of Windows Updates log file %logfile% +) diff --git a/tools/devboxes/windows-2022/base/scripts/disable-screensaver.ps1 b/tools/devboxes/windows-2022/base/scripts/disable-screensaver.ps1 new file mode 100644 index 00000000000..dd0a02900d2 --- /dev/null +++ b/tools/devboxes/windows-2022/base/scripts/disable-screensaver.ps1 @@ -0,0 +1,4 @@ +Write-Output "Disabling Screensaver" +Set-ItemProperty "HKCU:\Control Panel\Desktop" -Name ScreenSaveActive -Value 0 -Type DWord +& powercfg -x -monitor-timeout-ac 0 +& powercfg -x -monitor-timeout-dc 0 diff --git a/tools/devboxes/windows-2022/base/scripts/disable-winrm.ps1 b/tools/devboxes/windows-2022/base/scripts/disable-winrm.ps1 new file mode 100644 index 00000000000..28b92bd5ba7 --- /dev/null +++ b/tools/devboxes/windows-2022/base/scripts/disable-winrm.ps1 @@ -0,0 +1,8 @@ +netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new enable=yes action=block +netsh advfirewall firewall set rule group="Windows Remote Management" new enable=yes +$winrmService = Get-Service -Name WinRM +if ($winrmService.Status -eq "Running") { + Disable-PSRemoting -Force +} +Stop-Service winrm +Set-Service -Name winrm -StartupType Disabled diff --git a/tools/devboxes/windows-2022/base/scripts/enable-rdp.bat b/tools/devboxes/windows-2022/base/scripts/enable-rdp.bat new file mode 100644 index 00000000000..f7dcaab5c54 --- /dev/null +++ b/tools/devboxes/windows-2022/base/scripts/enable-rdp.bat @@ -0,0 +1,2 @@ +netsh advfirewall firewall add rule name="Open Port 3389" dir=in action=allow protocol=TCP localport=3389 +reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 0 /f diff --git a/tools/devboxes/windows-2022/base/scripts/enable-winrm.ps1 b/tools/devboxes/windows-2022/base/scripts/enable-winrm.ps1 new file mode 100644 index 00000000000..251b3eba72b --- /dev/null +++ b/tools/devboxes/windows-2022/base/scripts/enable-winrm.ps1 @@ -0,0 +1,15 @@ +Get-NetConnectionProfile | Set-NetConnectionProfile -NetworkCategory Private + +Enable-PSRemoting -Force +winrm quickconfig -q +winrm quickconfig -transport:http +winrm set winrm/config '@{MaxTimeoutms="1800000"}' +winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="800"}' +winrm set winrm/config/service '@{AllowUnencrypted="true"}' +winrm set winrm/config/service/auth '@{Basic="true"}' +winrm set winrm/config/client/auth '@{Basic="true"}' +winrm set winrm/config/listener?Address=*+Transport=HTTP '@{Port="5985"}' +netsh advfirewall firewall set rule group="Windows Remote Administration" new enable=yes +netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new enable=yes action=allow remoteip=any +Set-Service winrm -startuptype "auto" +Restart-Service winrm diff --git a/tools/devboxes/windows-2022/base/scripts/microsoft-updates.bat b/tools/devboxes/windows-2022/base/scripts/microsoft-updates.bat new file mode 100644 index 00000000000..edb849f4920 --- /dev/null +++ b/tools/devboxes/windows-2022/base/scripts/microsoft-updates.bat @@ -0,0 +1,12 @@ +net stop wuauserv + +reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update" /v EnableFeaturedSoftware /t REG_DWORD /d 1 /f + +reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update" /v IncludeRecommendedUpdates /t REG_DWORD /d 1 /f + +echo Set ServiceManager = CreateObject("Microsoft.Update.ServiceManager") > A:\temp.vbs +echo Set NewUpdateService = ServiceManager.AddService2("7971f918-a847-4430-9279-4a52d1efe18d",7,"") >> A:\temp.vbs + +cscript A:\temp.vbs + +net start wuauserv diff --git a/tools/devboxes/windows-2022/base/scripts/set-winrm-automatic.bat b/tools/devboxes/windows-2022/base/scripts/set-winrm-automatic.bat new file mode 100644 index 00000000000..fba5809de01 --- /dev/null +++ b/tools/devboxes/windows-2022/base/scripts/set-winrm-automatic.bat @@ -0,0 +1,2 @@ +echo Set WinRM start type to auto +sc config winrm start= auto diff --git a/tools/devboxes/windows-2022/base/scripts/uac-enable.bat b/tools/devboxes/windows-2022/base/scripts/uac-enable.bat new file mode 100644 index 00000000000..278ac00089a --- /dev/null +++ b/tools/devboxes/windows-2022/base/scripts/uac-enable.bat @@ -0,0 +1 @@ +reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /f /v EnableLUA /t REG_DWORD /d 1 diff --git a/tools/devboxes/windows-2022/base/scripts/win-updates.ps1 b/tools/devboxes/windows-2022/base/scripts/win-updates.ps1 new file mode 100644 index 00000000000..2091a69e173 --- /dev/null +++ b/tools/devboxes/windows-2022/base/scripts/win-updates.ps1 @@ -0,0 +1,252 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] +param($global:RestartRequired = 0, + $global:MoreUpdates = 0, + $global:MaxCycles = 5, + $MaxUpdatesPerCycle = 500, + $BeginWithRestart = 0) + +$Logfile = "C:\Windows\Temp\win-updates.log" + +function LogWrite { + Param ([string]$logstring) + $now = Get-Date -format s + Add-Content $Logfile -value "$now $logstring" + Write-Output $logstring +} + +function Check-ContinueRestartOrEnd() { + $RegistryKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" + $RegistryEntry = "InstallWindowsUpdates" + switch ($global:RestartRequired) { + 0 { + $prop = (Get-ItemProperty $RegistryKey).$RegistryEntry + if ($prop) { + LogWrite "Restart Registry Entry Exists - Removing It" + Remove-ItemProperty -Path $RegistryKey -Name $RegistryEntry -ErrorAction SilentlyContinue + } + + LogWrite "No Restart Required" + Check-WindowsUpdates + + if (($global:MoreUpdates -eq 1) -and ($script:Cycles -le $global:MaxCycles)) { + Install-WindowsUpdates + } + elseif ($script:Cycles -gt $global:MaxCycles) { + LogWrite "Exceeded Cycle Count - Stopping" + & "a:\enable-winrm.ps1" + } + else { + LogWrite "Done Installing Windows Updates" + & "a:\enable-winrm.ps1" + } + } + 1 { + $prop = (Get-ItemProperty $RegistryKey).$RegistryEntry + if (-not $prop) { + LogWrite "Restart Registry Entry Does Not Exist - Creating It" + Set-ItemProperty -Path $RegistryKey -Name $RegistryEntry -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File $($script:ScriptPath) -MaxUpdatesPerCycle $($MaxUpdatesPerCycle)" + } + else { + LogWrite "Restart Registry Entry Exists Already" + } + + LogWrite "Restart Required - Restarting..." + Restart-Computer + } + default { + LogWrite "Unsure If A Restart Is Required" + break + } + } +} + +function Install-WindowsUpdates() + { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] + param() + $script:Cycles++ + LogWrite "Evaluating Available Updates with limit of $($MaxUpdatesPerCycle):" + $UpdatesToDownload = New-Object -ComObject 'Microsoft.Update.UpdateColl' + $script:i = 0; + $CurrentUpdates = $SearchResult.Updates + while ($script:i -lt $CurrentUpdates.Count -and $script:CycleUpdateCount -lt $MaxUpdatesPerCycle) { + $Update = $CurrentUpdates.Item($script:i) + if ($null -ne $Update) { + [bool]$addThisUpdate = $false + if ($Update.InstallationBehavior.CanRequestUserInput) { + LogWrite "> Skipping: $($Update.Title) because it requires user input" + } + else { + if (!($Update.EulaAccepted)) { + LogWrite "> Note: $($Update.Title) has a license agreement that must be accepted. Accepting the license." + $Update.AcceptEula() + [bool]$addThisUpdate = $true + $script:CycleUpdateCount++ + } + else { + [bool]$addThisUpdate = $true + $script:CycleUpdateCount++ + } + } + + if ([bool]$addThisUpdate) { + LogWrite "Adding: $($Update.Title)" + $UpdatesToDownload.Add($Update) | Out-Null + } + } + $script:i++ + } + + if ($UpdatesToDownload.Count -eq 0) { + LogWrite "No Updates To Download..." + } + else { + LogWrite 'Downloading Updates...' + $ok = 0; + while (! $ok) { + try { + $Downloader = $UpdateSession.CreateUpdateDownloader() + $Downloader.Updates = $UpdatesToDownload + $Downloader.Download() + $ok = 1; + } + catch { + LogWrite $_.Exception | Format-List -force + LogWrite "Error downloading updates. Retrying in 30s." + $script:attempts = $script:attempts + 1 + Start-Sleep -s 30 + } + } + } + + $UpdatesToInstall = New-Object -ComObject 'Microsoft.Update.UpdateColl' + [bool]$rebootMayBeRequired = $false + LogWrite 'The following updates are downloaded and ready to be installed:' + foreach ($Update in $SearchResult.Updates) { + if (($Update.IsDownloaded)) { + LogWrite "> $($Update.Title)" + $UpdatesToInstall.Add($Update) | Out-Null + + if ($Update.InstallationBehavior.RebootBehavior -gt 0) { + [bool]$rebootMayBeRequired = $true + } + } + } + + if ($UpdatesToInstall.Count -eq 0) { + LogWrite 'No updates available to install...' + $global:MoreUpdates = 0 + $global:RestartRequired = 0 + & "a:\enable-winrm.ps1" + break + } + + if ($rebootMayBeRequired) { + LogWrite 'These updates may require a reboot' + $global:RestartRequired = 1 + } + + LogWrite 'Installing updates...' + + $Installer = $script:UpdateSession.CreateUpdateInstaller() + $Installer.Updates = $UpdatesToInstall + $InstallationResult = $Installer.Install() + + LogWrite "Installation Result: $($InstallationResult.ResultCode)" + LogWrite "Reboot Required: $($InstallationResult.RebootRequired)" + LogWrite 'Listing of updates installed and individual installation results:' + if ($InstallationResult.RebootRequired) { + $global:RestartRequired = 1 + } + else { + $global:RestartRequired = 0 + } + + for ($i = 0; $i -lt $UpdatesToInstall.Count; $i++) { + New-Object -TypeName PSObject -Property @{ + Title = $UpdatesToInstall.Item($i).Title + Result = $InstallationResult.GetUpdateResult($i).ResultCode + } + LogWrite "Item: $($UpdatesToInstall.Item($i).Title)" + LogWrite "Result: $($InstallationResult.GetUpdateResult($i).ResultCode)" + } + + Check-ContinueRestartOrEnd +} + +function Check-WindowsUpdates() { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] + param() + LogWrite "Checking For Windows Updates" + $Username = $env:USERDOMAIN + "\" + $env:USERNAME + LogWrite "Script: $script:ScriptPath `nScript User: $Username `nStarted: $(Get-Date)" + + $script:UpdateSearcher = $script:UpdateSession.CreateUpdateSearcher() + $script:successful = $FALSE + $script:attempts = 0 + $script:maxAttempts = 12 + while (-not $script:successful -and $script:attempts -lt $script:maxAttempts) { + try { + $script:SearchResult = $script:UpdateSearcher.Search("IsInstalled=0 and Type='Software' and IsHidden=0") + $script:successful = $TRUE + } + catch { + LogWrite $_.Exception | Format-List -force + LogWrite "Search call to UpdateSearcher was unsuccessful. Retrying in 10s." + $script:attempts = $script:attempts + 1 + Start-Sleep -s 10 + } + } + + if ($SearchResult.Updates.Count -ne 0) { + $Message = "There are " + $SearchResult.Updates.Count + " more updates." + LogWrite $Message + try { + for ($i = 0; $i -lt $script:SearchResult.Updates.Count; $i++) { + LogWrite $script:SearchResult.Updates.Item($i).Title + LogWrite $script:SearchResult.Updates.Item($i).Description + LogWrite $script:SearchResult.Updates.Item($i).RebootRequired + LogWrite $script:SearchResult.Updates.Item($i).EulaAccepted + } + $global:MoreUpdates = 1 + } + catch { + LogWrite $_.Exception | Format-List -force + LogWrite "Showing SearchResult was unsuccessful. Rebooting." + $global:RestartRequired = 1 + $global:MoreUpdates = 0 + Check-ContinueRestartOrEnd + LogWrite "Show never happen to see this text!" + Restart-Computer + } + } + else { + LogWrite 'There are no applicable updates' + $global:RestartRequired = 0 + $global:MoreUpdates = 0 + } +} + +$script:ScriptName = $MyInvocation.MyCommand.ToString() +$script:ScriptPath = $MyInvocation.MyCommand.Path +$script:UpdateSession = New-Object -ComObject 'Microsoft.Update.Session' +$script:UpdateSession.ClientApplicationID = 'Packer Windows Update Installer' +$script:UpdateSearcher = $script:UpdateSession.CreateUpdateSearcher() +$script:SearchResult = New-Object -ComObject 'Microsoft.Update.UpdateColl' +$script:Cycles = 0 +$script:CycleUpdateCount = 0 + +if ($BeginWithRestart) { + $global:RestartRequired = 1 + Check-ContinueRestartOrEnd +} + +Check-WindowsUpdates +if ($global:MoreUpdates -eq 1) { + Install-WindowsUpdates +} +else { + Check-ContinueRestartOrEnd +} diff --git a/tools/devboxes/windows-2022/base/windows-2022-base.pkr.hcl b/tools/devboxes/windows-2022/base/windows-2022-base.pkr.hcl new file mode 100644 index 00000000000..743330c8dde --- /dev/null +++ b/tools/devboxes/windows-2022/base/windows-2022-base.pkr.hcl @@ -0,0 +1,160 @@ +# Windows Server 2022 Desktop installation template +# +# This was originally bootstrapped by copying +# https://github.com/StefanScherer/packer-windows/blob/main/windows_2022.json. I then converted it +# to HCL, de-Vagranted it, and removed several scripts which weren't useful for our use case. + +packer { + required_plugins { + qemu = { + source = "github.com/hashicorp/qemu" + version = "~> 1.1.0" + } + } +} + +variables { + cpus = 2 + # Disk size must match the disk size in windows-2022.pkr.hcl, or else you will get a BSOD! + disk_size = 100000 + headless = false + memory = 8192 +} + +# The `installer_iso` variable contains a link to the current Windows Server 2022 +# English (United States) ISO image. +# +# To update the link: +# +# 1. Visit https://www.microsoft.com/en-us/evalcenter/download-windows-server-2022. +# +# 2. Use `curl` to GET the URL for the English (United States) ISO. Expect to receive a +# 301 Moved Permanently response from the origin. +# +# 3. Copy the response's Location header value to this variable's default value below. +# +# 4. Run `packer build`. When the checksum fails, make note of the new checksum, and update the +# `installer_iso_checksum` variable's default value below. +variable "installer_iso" { + type = string + default = "https://software-static.download.prss.microsoft.com/sg/download/888969d5-f34g-4e03-ac9d-1f9786c66749/SERVER_EVAL_x64FRE_en-us.iso" +} + +variable "installer_iso_checksum" { + type = string + default = "sha256:3e4fa6d8507b554856fc9ca6079cc402df11a8b79344871669f0251535255325" +} + +# The `virtio_win_iso` variable identifies the path to the VirtIO Windows drivers ISO. It is +# mandatory. +# +# To set this variable, run the `fetch-deps.sh` script next to the main `windows-2022.pkr.hcl` +# Packer template. The script will download the VirtIO ISO file into the right place, and create +# a deps.auto.pkrvars.hcl file next to _this_ Packer template which sets this variable. +variable "virtio_win_iso" { + description = "Path to virtio-win.iso" + type = string + default = "" + + validation { + condition = length(var.virtio_win_iso) > 0 + error_message = "Run `fetch-deps.sh` to set this variable." + } + + validation { + condition = fileexists(var.virtio_win_iso) + error_message = "`virtio_win_iso` file does not exist. Run `fetch-deps.sh` to set this variable." + } +} + +# The `output_qcow2` variable is the name of the generated VM snapshot, ready for importing into +# virt-manager, or running directly with QEMU. +variable "output_qcow2" { + type = string + default = "windows-2022-base.qcow2" +} + +locals { + output_directory = "output-windows-2022-base" +} + +source "qemu" "windows-2022-base" { + # Hardware + accelerator = "kvm" + headless = var.headless + cpus = var.cpus + memory = var.memory + disk_size = var.disk_size + + # Input files + floppy_files = [ + # The Autounattend.xml chooses whether to install 2022 Desktop or Core. + "${path.root}/answer_files/2022/Autounattend.xml", + "${path.root}/scripts/disable-screensaver.ps1", + "${path.root}/scripts/disable-winrm.ps1", + "${path.root}/scripts/enable-winrm.ps1", + "${path.root}/scripts/microsoft-updates.bat", + "${path.root}/scripts/win-updates.ps1" + ] + + # Input image + iso_url = var.installer_iso + iso_checksum = var.installer_iso_checksum + + # Output image + output_directory = local.output_directory + vm_name = var.output_qcow2 + format = "qcow2" + + # Since we need to attach multiple drives, we must override `qemuargs`. This ends up overriding + # a lot of other disk-related configuration properties in the `qemu` data source. + qemuargs = [ + ["-drive", "file=${local.output_directory}/${var.output_qcow2},if=virtio,cache=writeback,discard=ignore,format=qcow2,index=1"], + ["-drive", "file=${var.installer_iso},media=cdrom,index=2"], + ["-drive", "file=${var.virtio_win_iso},media=cdrom,index=3"] + ] + + # Give a little time for QEMU's VNC server to start. + boot_wait = "1s" + + communicator = "winrm" + winrm_password = "vagrant" + winrm_username = "vagrant" + # We have to wait for Windows to install itself before we can connect with WinRM. This typically + # takes 10 to 30 minutes on my machine. + winrm_timeout = "2h" + + shutdown_command = "shutdown /s /t 10 /f /d p:4:1 /c \"Packer Shutdown\"" +} + +build { + sources = ["source.qemu.windows-2022-base"] + + provisioner "windows-shell" { + scripts = ["${path.root}/scripts/enable-rdp.bat"] + } + + provisioner "windows-shell" { + scripts = [ + "${path.root}/scripts/set-winrm-automatic.bat", + "${path.root}/scripts/uac-enable.bat", + "${path.root}/scripts/dis-updates.bat", + ] + } + + # We make the resulting image read-only, with the idea that you should use it as a backing file + # for a new copy-on-write image. This allows you to easily revert to a pristine snapshot of a + # freshly-installed Windows at any time. + post-processor "shell-local" { + inline = [ + "chmod 0444 ${local.output_directory}/${var.output_qcow2}", + # Run qemu-img to make backing file. + # Print something out. + ] + } + + post-processor "checksum" { + checksum_types = ["sha256"] + output = "${local.output_directory}/${var.output_qcow2}.{{.ChecksumType}}" + } +} diff --git a/tools/devboxes/windows-2022/deps.aria2-input b/tools/devboxes/windows-2022/deps.aria2-input new file mode 100644 index 00000000000..84df0d67e55 --- /dev/null +++ b/tools/devboxes/windows-2022/deps.aria2-input @@ -0,0 +1,49 @@ +# Input file for aria2c CLI download tool. Run like so: +# aria2c --check-integrity=true --input-file <this file> +# +# The --check-integrity=true parameter tells aria2c not to re-download a file if it exists and its +# checksum matches the one in this file. + +# ---------------------------------------------------------- +# windows-2022/base dependencies + +# Virtio driver ISO. These drivers allow guests to use more performant virtualized hardware that +# QEMU has to offer. The ISO also includes virtiofsd, which, when installed, allows you to configure +# a shared folder with the Windows guest OS, as well as QEMU and SPICE guest agents. +# +# Depending on how you construct the link, you may: +# +# - Un-pin, using whatever the latest stable version is: .../stable-virtio/virtio-win.iso +# - Pin to the latest stable version, breaking on update: .../stable-virtio/virtio-win-x.y.z.iso +# - Pin to a specific version: .../archive-virtio/virtio-win-0.1.266-1/virtio-win-0.1.266.iso +# +# You can find links to the latest stable version, and version archives, here: +# - https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/ +# - https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/ +https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.266-1/virtio-win-0.1.266.iso + dir=base/deps + out=virtio-win.iso + checksum=sha-256=57b0f6dc8dc92dc2ae8621f8b1bfbd8a873de9bedc788c4c4b305ea28acc77cd + +# ---------------------------------------------------------- +# windows-2022 dependencies + +# Virtiofs service dependencies +https://github.com/winfsp/winfsp/releases/download/v2.0/winfsp-2.0.23075.msi + dir=deps + out=winfsp.msi + checksum=sha-256=6324dc81194a6a08f97b6aeca303cf5c2325c53ede153bae9fc4378f0838c101 + +# Winget dependencies +https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx + dir=deps + out=Microsoft.VCLibs.x64.14.Desktop.zip + checksum=sha-256=b56a9101f706f9d95f815f5b7fa6efbac972e86573d378b96a07cff5540c5961 +https://www.nuget.org/api/v2/package/Microsoft.UI.Xaml/2.7.1 + dir=deps + out=Microsoft.UI.Xaml.zip + checksum=sha-256=4ffd18beb1e26dff9f5bdfc8762882efbda3c241952efadbb7d1280e4796f85d +https://github.com/microsoft/winget-cli/releases/download/v1.9.25200/Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle + dir=deps + out=Microsoft.DesktopAppInstaller.zip + checksum=sha-256=46d46bb5deacef0fd8ac30a223072b45ac2d5d5262d1591f2c08fb6ee15e4b22 diff --git a/tools/devboxes/windows-2022/fetch-deps.sh b/tools/devboxes/windows-2022/fetch-deps.sh new file mode 100755 index 00000000000..7af2b40476e --- /dev/null +++ b/tools/devboxes/windows-2022/fetch-deps.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# This is a helper script various binary depdenecies for the windows-2022 and windows-2022/base +# Packer templates. + +set -euo pipefail + +# Download relative to this script. +ABS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Remove any old Packer configuration for these deps. +rm "$ABS_SCRIPT_DIR/base/deps.auto.pkrvars.hcl" 2> /dev/null || true +rm "$ABS_SCRIPT_DIR/deps.auto.pkrvars.hcl" 2> /dev/null || true + +# Download. +(cd "$ABS_SCRIPT_DIR" && aria2c --check-integrity --input-file deps.aria2-input) + +# Configure windows-2022/base +echo "Writing base/deps.auto.pkrvars.hcl" +cat << EOF > "$ABS_SCRIPT_DIR/base/deps.auto.pkrvars.hcl" +virtio_win_iso = "$ABS_SCRIPT_DIR/base/deps/virtio-win.iso" +EOF + +# Configure windows-2022 +echo "Writing deps.auto.pkrvars.hcl" +cat << EOF > "$ABS_SCRIPT_DIR/deps.auto.pkrvars.hcl" +deps_dir = "$ABS_SCRIPT_DIR/deps" +EOF + +printf "Packer templates now configured. Build with \`packer build $ABS_SCRIPT_DIR/base && packer build $ABS_SCRIPT_DIR\`.\n" diff --git a/tools/devboxes/windows-2022/files/use-system-trust-store.bazelrc b/tools/devboxes/windows-2022/files/use-system-trust-store.bazelrc new file mode 100644 index 00000000000..eb104571e45 --- /dev/null +++ b/tools/devboxes/windows-2022/files/use-system-trust-store.bazelrc @@ -0,0 +1,4 @@ +# %ProgramData%\bazel.bazelrc + +# Use the Windows trust store instead of the JDK's builtin one. +startup --host_jvm_args=-Djavax.net.ssl.trustStoreType=Windows-ROOT diff --git a/tools/devboxes/windows-2022/scripts/fix-virtiofs.ps1 b/tools/devboxes/windows-2022/scripts/fix-virtiofs.ps1 new file mode 100644 index 00000000000..f74d38030a3 --- /dev/null +++ b/tools/devboxes/windows-2022/scripts/fix-virtiofs.ps1 @@ -0,0 +1,5 @@ +# VirtioFsSvc depends on WinFsp being installed. +cmd /c msiexec /qb /i E:\deps\winfsp.msi + +# VirtioFsSvc should already have been installed by virtio-win-guest-tools.exe. +Set-Service -Name VirtioFsSvc -StartupType Automatic diff --git a/tools/devboxes/windows-2022/scripts/install-custom-root-ca.ps1 b/tools/devboxes/windows-2022/scripts/install-custom-root-ca.ps1 new file mode 100644 index 00000000000..50af13622f8 --- /dev/null +++ b/tools/devboxes/windows-2022/scripts/install-custom-root-ca.ps1 @@ -0,0 +1,3 @@ +if ($env:CUSTOM_ROOT_CA.length -gt 0) { + Import-Certificate -FilePath "E:\$env:CUSTOM_ROOT_CA" -CertStoreLocation Cert:\LocalMachine\Root +} diff --git a/tools/devboxes/windows-2022/scripts/install-git.ps1 b/tools/devboxes/windows-2022/scripts/install-git.ps1 new file mode 100644 index 00000000000..32db26f3341 --- /dev/null +++ b/tools/devboxes/windows-2022/scripts/install-git.ps1 @@ -0,0 +1,15 @@ +winget install -e --id Git.Git --version 2.47.1 --accept-source-agreements --accept-package-agreements + +$gitPath = "C:\Program Files\Git\bin" + +$systemEnvPath = [System.Environment]::GetEnvironmentVariable('PATH', [System.EnvironmentVariableTarget]::Machine) +$systemEnvPath = "$gitPath;$systemEnvPath" +[System.Environment]::SetEnvironmentVariable('PATH', $systemEnvPath, [System.EnvironmentVariableTarget]::Machine) + +Write-Output "Added $gitPath to the system PATH." + +$env:PATH = "$gitPath;$env:PATH" + +# Configure git to use the Windows Trust Store, to pick up any custom root CA we installed. +Write-Output "Configuring Git to use the Windows Trust Store (schannel)" +git config --global http.sslBackend schannel diff --git a/tools/devboxes/windows-2022/scripts/install-msys.ps1 b/tools/devboxes/windows-2022/scripts/install-msys.ps1 new file mode 100644 index 00000000000..52863d791c1 --- /dev/null +++ b/tools/devboxes/windows-2022/scripts/install-msys.ps1 @@ -0,0 +1,19 @@ +winget install -e --id MSYS2.MSYS2 --version 20241116 --accept-source-agreements --accept-package-agreements + +$msysPath = "C:\msys64\usr\bin" + +$systemEnvPath = [System.Environment]::GetEnvironmentVariable('PATH', [System.EnvironmentVariableTarget]::Machine) +$systemEnvPath = "$msysPath;$systemEnvPath" +[System.Environment]::SetEnvironmentVariable('PATH', $systemEnvPath, [System.EnvironmentVariableTarget]::Machine) + +Write-Output "Added $msysPath to the system PATH." + +$env:PATH = "$msysPath;$env:PATH" + +# MSYS has its own trust store, separate from Windows. +# https://www.msys2.org/docs/faq/ +if ($env:CUSTOM_ROOT_CA.length -gt 0) { + Write-Output "Adding custom root CA to MSYS /etc/pki/ca-trust/source/anchors/" + Copy-Item "E:\$env:CUSTOM_ROOT_CA" "C:\msys64\etc\pki\ca-trust\source\anchors\" + C:\msys64\usr\bin\bash.exe -c /usr/bin/update-ca-trust +} diff --git a/tools/devboxes/windows-2022/scripts/install-posh-git.ps1 b/tools/devboxes/windows-2022/scripts/install-posh-git.ps1 new file mode 100644 index 00000000000..65d4464848f --- /dev/null +++ b/tools/devboxes/windows-2022/scripts/install-posh-git.ps1 @@ -0,0 +1,56 @@ +# Borrowed from https://github.com/asheroto/winget-install/blob/master/winget-install.ps1 +function Install-NuGetIfRequired { + <# + .SYNOPSIS + Checks if the NuGet PackageProvider is installed and installs it if required. + + .DESCRIPTION + This function checks whether the NuGet PackageProvider is already installed on the system. If it is not found and the current PowerShell version is less than 7, it attempts to install the NuGet provider using Install-PackageProvider. + For PowerShell 7 or greater, it assumes NuGet is available by default and advises reinstallation if NuGet is missing. + + .PARAMETER Debug + Enables debug output for additional details during installation. + + .EXAMPLE + Install-NuGetIfRequired + # Checks for the NuGet provider and installs it if necessary. + + .NOTES + This function only attempts to install NuGet if the PowerShell version is less than 7. + For PowerShell 7 or greater, NuGet is typically included by default and does not require installation. + #> + + # Check if NuGet PackageProvider is already installed, skip package provider installation if found + if (-not (Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)) { + Write-Debug "NuGet PackageProvider not found." + + # Check if running in PowerShell version less than 7 + if ($PSVersionTable.PSVersion.Major -lt 7) { + # Install NuGet PackageProvider if running PowerShell version less than 7 + # PowerShell 7 has limited support for installing package providers, but NuGet is available by default in PowerShell 7 so installation is not required + + Write-Debug "Installing NuGet PackageProvider..." + + if ($Debug) { + try { Install-PackageProvider -Name "NuGet" -Force -ForceBootstrap -ErrorAction SilentlyContinue } catch { } + } else { + try { Install-PackageProvider -Name "NuGet" -Force -ForceBootstrap -ErrorAction SilentlyContinue | Out-Null } catch {} + } + } else { + # NuGet should be included by default in PowerShell 7, so if it's not detected, advise reinstallation + Write-Warning "NuGet is not detected in PowerShell 7. Consider reinstalling PowerShell 7, as NuGet should be included by default." + } + } else { + # NuGet PackageProvider is already installed + Write-Debug "NuGet PackageProvider is already installed. Skipping installation." + } +} + +Write-Debug "Checking if NuGet PackageProvider is already installed..." +Install-NuGetIfRequired + +PowerShellGet\Install-Module posh-git -Scope CurrentUser -Force +Add-PoshGitToProfile -AllUsers -AllHosts + +# $profileContent = "`n$GitPromptSettings.DefaultPromptAbbreviateHomeDirectory = $true" +# Add-Content -LiteralPath $profile.AllUsersAllHosts -Value $profileContent -Encoding UTF8 diff --git a/tools/devboxes/windows-2022/scripts/install-ssh.ps1 b/tools/devboxes/windows-2022/scripts/install-ssh.ps1 new file mode 100644 index 00000000000..ef90138a220 --- /dev/null +++ b/tools/devboxes/windows-2022/scripts/install-ssh.ps1 @@ -0,0 +1,4 @@ +# https://github.com/PowerShell/Win32-OpenSSH/wiki/Install-Win32-OpenSSH +winget install -e --id Microsoft.OpenSSH.Beta --version 9.5.0.0 --accept-source-agreements --accept-package-agreements + +New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 diff --git a/tools/devboxes/windows-2022/scripts/install-winget-the-crappy-way.ps1 b/tools/devboxes/windows-2022/scripts/install-winget-the-crappy-way.ps1 new file mode 100644 index 00000000000..f47655dd93a --- /dev/null +++ b/tools/devboxes/windows-2022/scripts/install-winget-the-crappy-way.ps1 @@ -0,0 +1,37 @@ +# This script appears to be the only way to install Winget in such a way that Packer can run it over +# WinRM. Original source: https://github.com/microsoft/winget-cli/issues/256#issuecomment-1416929101 + +Write-Output "Extracting WinGet and dependencies..." + +New-Item -Path "$ENV:USERPROFILE" -Name "build" -ItemType "directory" -Force | Out-Null + +Expand-Archive -Path "E:/deps/Microsoft.VCLibs.x64.14.Desktop.zip" -DestinationPath "$ENV:USERPROFILE/build/WinGet" -Force + +Expand-Archive -Path "E:/deps/Microsoft.UI.Xaml.zip" -DestinationPath "$ENV:USERPROFILE/build/Microsoft.UI.Xaml" +Rename-Item -Path "$ENV:USERPROFILE/build/Microsoft.UI.Xaml/tools/AppX/x64/Release/Microsoft.UI.Xaml.2.7.appx" -NewName "Microsoft.UI.Xaml.2.7.zip" +Expand-Archive -Path "$ENV:USERPROFILE/build/Microsoft.UI.Xaml/tools/AppX/x64/Release/Microsoft.UI.Xaml.2.7.zip" -DestinationPath "$ENV:USERPROFILE/build/WinGet" -Force + +Expand-Archive -Path "E:/deps/Microsoft.DesktopAppInstaller.zip" -DestinationPath "$ENV:USERPROFILE/build/Microsoft.DesktopAppInstaller" +Rename-Item -Path "$ENV:USERPROFILE/build/Microsoft.DesktopAppInstaller/AppInstaller_x64.msix" -NewName "AppInstaller_x64.zip" +Expand-Archive -Path "$ENV:USERPROFILE/build/Microsoft.DesktopAppInstaller/AppInstaller_x64.zip" -DestinationPath "$ENV:USERPROFILE/build/WinGet" -Force + +Move-Item -Path "$ENV:USERPROFILE/build/WinGet" -Destination "$ENV:PROGRAMFILES" -PassThru +cmd /c setx /M PATH "%PATH%;$ENV:PROGRAMFILES\WinGet" + +# I originally tried to install Winget "the right way" ... +# +# The most robust way to install Winget seems to be use this script: +# https://github.com/asheroto/winget-install +# +# On Windows Server 2022, the above winget-install script boils down to the method described here: +# https://github.com/microsoft/winget-cli/issues/4390 +# +# But there are a couple catches: +# - The above methods use the Microsoft Store to install Winget for all users. Winget is not +# available for a user until the user has logged in interactively for the first time, at which +# point an asynchronous registration process starts, and Winget.exe eventually appears in the +# user's PATH. To start using it immediately, you can force registration with a PowerShell +# cmdlet: Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe +# See: https://learn.microsoft.com/en-us/windows/package-manager/winget/#install-winget +# - Installed this way, Winget is not usable over WinRM: +# https://github.com/microsoft/winget-cli/issues/256 diff --git a/tools/devboxes/windows-2022/scripts/update-root-trust-store.ps1 b/tools/devboxes/windows-2022/scripts/update-root-trust-store.ps1 new file mode 100644 index 00000000000..d1bed10a656 --- /dev/null +++ b/tools/devboxes/windows-2022/scripts/update-root-trust-store.ps1 @@ -0,0 +1,19 @@ +# Windows only ships with a handful of trusted root certificates, then lazily-loads the rest from +# Windows Update as needed. +# +# This script downloads all the latest trusted root certificates from Windows Update proactively, so +# that tools which don't know how to trigger Windows' lazy-loading behavior (like Bazel) can use the +# Windows trust store right away. +# +# Sources: +# - https://woshub.com/updating-trusted-root-certificates-in-windows-10/#h2_3 +# - https://stackoverflow.com/a/72969049 + +# Download certificates from Windows Update. +certutil.exe -generateSSTFromWU $env:TEMP\roots.sst + +# Import the certificates into the root trust store. +$sstFile = (Get-ChildItem -Path $env:TEMP\roots.sst) +$sstFile | Import-Certificate -CertStoreLocation Cert:\LocalMachine\Root + +Remove-Item -Path $env:TEMP\roots.sst diff --git a/tools/devboxes/windows-2022/windows-2022.pkr.hcl b/tools/devboxes/windows-2022/windows-2022.pkr.hcl new file mode 100644 index 00000000000..da22d90c0e2 --- /dev/null +++ b/tools/devboxes/windows-2022/windows-2022.pkr.hcl @@ -0,0 +1,135 @@ +# Windows Server 2022 Desktop provisioning template +# +# This template takes the output image of the `base/` template (relative to this directory), and +# configures a custom root CA and installs various tools useful for workerd development. + +packer { + required_plugins { + qemu = { + version = "~> 1.1.0" + source = "github.com/hashicorp/qemu" + } + } +} + +variables { + cpus = 2 + # Disk size must match the disk size in windows-2022-base.pkr.hcl, or else you will get a BSOD! + disk_size = 100000 + headless = false + memory = 8192 +} + +# The `custom_root_ca` variable allows you to add a custom trusted root CA to the Windows guest's +# trust store. +# +# If your organization uses a custom root CA to inspect traffic, such as can be done with +# [Cloudflare One](https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/user-side-certificates/), +# you'll need to populate this variable with the path to the CA's PEM-encoded certificate. +# +# You must explicitly set this feature, because if you need a custom root CA, it's a pain if you +# accidentally build the image without one. +variable "custom_root_ca" { + type = string +} + +variable "deps_dir" { + description = "Path to deps/ with winfsp.msi, etc. inside" + type = string + default = "" + + validation { + condition = length(var.deps_dir) > 0 + error_message = "Run `fetch-deps.sh` to set this variable." + } +} + +variable "input_qcow2" { + type = string + default = "output-windows-2022-base/windows-2022-base.qcow2" +} + +variable "output_qcow2" { + type = string + default = "windows-2022.qcow2" +} + +locals { + # Read the SHA256 from the file produced by Packer's Checksum post-processor. + input_qcow2_sha256 = split("\t", file("${path.cwd}/${var.input_qcow2}.sha256"))[0] + output_directory = "output-windows-2022" +} + +source "qemu" "windows-2022" { + # Hardware + accelerator = "kvm" + headless = var.headless + cpus = var.cpus + memory = var.memory + disk_size = var.disk_size + + cd_files = flatten([ + # Optionally include a custom root CA. We do not check fileexists(), to guard against misspelled + # filenames. + length(var.custom_root_ca) > 0 ? [var.custom_root_ca] : [], + var.deps_dir + ]) + + # Input image + iso_url = var.input_qcow2 + iso_checksum = local.input_qcow2_sha256 + + # Output image + output_directory = local.output_directory + vm_name = var.output_qcow2 + format = "qcow2" + + # Use the input image as a backing file for the output image. + disk_image = true + use_backing_file = true + skip_resize_disk = true + skip_compaction = true + disk_compression = false + + # Give a little time for QEMU's VNC server to start. + boot_wait = "1s" + + communicator = "winrm" + winrm_username = "vagrant" + winrm_password = "vagrant" + # We expect the machine to boot reasonably quickly. + winrm_timeout = "10m" + + shutdown_command = "shutdown /s /t 10 /f /d p:4:1 /c \"Packer Shutdown\"" +} + +build { + sources = ["source.qemu.windows-2022"] + + provisioner "powershell" { + environment_vars = ["CUSTOM_ROOT_CA=${basename(var.custom_root_ca)}"] + scripts = [ + # We install our own custom root CA first, in case traffic to Windows Update is secured by + # our custom root CA. + "${path.root}/scripts/install-custom-root-ca.ps1", + # Now we can update the root trust store with the latest certificates available in WU. + "${path.root}/scripts/update-root-trust-store.ps1", + "${path.root}/scripts/fix-virtiofs.ps1", + "${path.root}/scripts/install-winget-the-crappy-way.ps1", + "${path.root}/scripts/install-ssh.ps1", + # Git and MSYS both come with Bash environments. Each install script prepends their /bin/ to + # the existing PATH, so earlier scripts' environments will be found later in the PATH. I + # listed them this way because that is how things would end up if you only had Git installed, + # cloned workerd, and ran its install-deps.bat script. I'm not sure if it's the best answer. + "${path.root}/scripts/install-git.ps1", + "${path.root}/scripts/install-msys.ps1", + "${path.root}/scripts/install-posh-git.ps1" + ] + } + + # Configure Bazel to use the system trust store, when it gets installed later. + provisioner "file" { + source = "${path.root}/files/use-system-trust-store.bazelrc" + destination = "C:\\ProgramData\\bazel.bazelrc" + } +}