Skip to content

POC: add Windows guest support via autounattend.xml#4985

Draft
ashwat287 wants to merge 1 commit into
lima-vm:masterfrom
ashwat287:autounattendedPOC
Draft

POC: add Windows guest support via autounattend.xml#4985
ashwat287 wants to merge 1 commit into
lima-vm:masterfrom
ashwat287:autounattendedPOC

Conversation

@ashwat287
Copy link
Copy Markdown
Contributor

@ashwat287 ashwat287 commented May 15, 2026

Updated with dual-arch support. Both amd64 and arm64 work end-to-end now. Ref: #4852

What's in this commit

  • WINDOWS OS type added to limatype and validation
  • Separate autounattend.xml templates for amd64 and arm64 (3-pass: windowsPE, specialize, oobeSystem)
  • Windows guests go through generateWindowsISO() which produces a Joliet-labeled ISO
  • hack/gen-autounattend-iso.go dev tool for standalone testing (pass -arch amd64 or -arch arm64)
  • Unit test for template rendering

Design choices

  • arm64 uses NVMe for the disk. WinPE ARM has stornvme.sys built-in, so there's no need for virtio-win.iso.
  • No product keys in either template. Removed per @AkihiroSuda's feedback. Edition selection is handled by image index.
  • OpenSSH server is installed and started during OOBE so Lima's host-agent can connect.

Testing

Tested both architectures on macOS.

amd64 (x86_64)

Windows 10 consumer ISO from microsoft.com, version 22H2 multi-edition. Pro is image index 6.

go run hack/gen-autounattend-iso.go -arch amd64 -o autounattend.iso
qemu-img create -f qcow2 disk.qcow2 64G
cp /opt/homebrew/share/qemu/edk2-i386-vars.fd ovmf_vars.fd

qemu-system-x86_64 -m 4G -smp 2 -machine q35 \
  -drive if=pflash,format=raw,readonly=on,file=/opt/homebrew/share/qemu/edk2-x86_64-code.fd \
  -drive if=pflash,format=raw,file=ovmf_vars.fd \
  -cdrom /path/to/Win10_22H2_English_x64v1.iso \
  -drive file=autounattend.iso,media=cdrom,index=3 \
  -drive file=disk.qcow2,index=0,media=disk,format=qcow2 \
  -nic user,hostfwd=tcp::2222-:22

This runs under emulation on Apple Silicon so it's slow, but completes successfully.

Screenshot 2026-05-15 at 2 35 07 AM

arm64 (aarch64)

Windows 11 ARM64 25H2 ISO. Pro is image index 3. Runs natively via HVF.

go run hack/gen-autounattend-iso.go -arch arm64 -o autounattend.iso
qemu-img create -f qcow2 disk.qcow2 64G
dd if=/dev/zero of=ovmf_vars.fd bs=1M count=64

qemu-system-aarch64 \
  -machine virt,accel=hvf \
  -cpu host \
  -m 4G -smp 4 \
  -drive if=pflash,format=raw,readonly=on,file=/opt/homebrew/share/qemu/edk2-aarch64-code.fd \
  -drive if=pflash,format=raw,file=ovmf_vars.fd \
  -drive file=disk.qcow2,if=none,id=hd0,format=qcow2 \
  -device nvme,serial=lima0,drive=hd0 \
  -device qemu-xhci \
  -device usb-kbd \
  -device usb-tablet \
  -drive file=/path/to/Win11_25H2_English_Arm64.iso,if=none,id=cdrom0,media=cdrom,readonly=on \
  -device usb-storage,drive=cdrom0 \
  -drive file=autounattend.iso,if=none,id=unattend,media=cdrom,readonly=on \
  -device usb-storage,drive=unattend \
  -device ramfb \
  -nic user,hostfwd=tcp::2222-:22

The arm64 command is longer because the virt machine has no built-in controllers (no SATA, no IDE, no VGA), so each device needs to be added explicitly. On amd64, q35 provides all of that.

Screenshot 2026-05-16 at 6 16 57 AM

Result

Both architectures install fully unattended: partitioning, image extraction, OOBE, and OpenSSH server all complete without manual interaction.

After install completes: ssh -p 2222 User@localhost

Notes

  • arm64 requires -cpu host, not -cpu max. Using max with HVF causes page faults.
  • UEFI variable store needs to be 64MB on arm64 (4MB on x86).
  • usb-kbd/usb-tablet are needed on arm64 for UEFI console initialization.

Changes since last review

  • Removed product key from both templates
  • Added arm64 template with NVMe disk (no virtio-win.iso needed)
  • Dropped -boot d from amd64 (OVMF ignores it)

@unsuman
Copy link
Copy Markdown
Member

unsuman commented May 15, 2026

Can you share info about your testing environment?

@ashwat287
Copy link
Copy Markdown
Contributor Author

ashwat287 commented May 15, 2026

Host Machine:

  • CPU: Apple M5 (arm64)
  • RAM: 16GB
  • OS: macOS Tahoe
  • Architecture: Apple Silicon

QEMU Setup:

  • Binary: qemu-system-x86_64 (emulates x86_64 guest on arm64 host)
  • Acceleration: HVF (Apple's Hypervisor.framework), but since guest arch != host arch, CPU instructions are emulated
  • Machine type: q35 (modern Intel chipset with PCIe, AHCI, ICH9)
  • RAM: 4GB (-m 4G)
  • vCPUs: 2 (-smp 2)
  • Install source: Homebrew QEMU package (brew install qemu)

Guest:

@ashwat287
Copy link
Copy Markdown
Contributor Author

ashwat287 commented May 15, 2026

Also tried to test in WSL2 before switching to macOS. But it failed probably because of nested virtualization.

The ideal way to test would be to test it in x86_64 linux machine to avoid additional performance baggage which comes with emulation. This I haven't tried yet.

Comment thread pkg/cidata/cidata.TEMPLATE.d.Windows/autounattend.xml Outdated
@AkihiroSuda
Copy link
Copy Markdown
Member

since guest arch != host arch, CPU instructions are emulated

Can we support the ARM version of Windows?

mn-ram added a commit to mn-ram/lima that referenced this pull request May 16, 2026
Schema groundwork for Lima's Windows-guest support. Without this, any
attempt to declare a Windows guest in YAML fails validation at the
first hurdle and there is no canonical template for reviewers to
exercise the rest of the pipeline against.

This commit:

- Adds `WINDOWS OS = "Windows"` to pkg/limatype/lima_yaml.go alongside
  the existing LINUX/DARWIN/FREEBSD constants, and includes it in the
  OSTypes slice that the validator consults.
- Teaches NewOS() to normalize the lowercase "windows" alias the same
  way it normalizes "linux" and "darwin".
- Whitelists limatype.WINDOWS in pkg/limayaml/validate.go so the
  switch on *y.OS accepts it.
- Updates TestValidateMultipleErrors to use "plan9" as the rejected OS
  (it was using "windows" as an example of an invalid value, which is
  now valid) and refreshes the expected OSTypes list in the error
  message.
- Adds TestValidateWindowsGuestOS, a focused positive-and-negative
  test for the new value.
- Adds templates/experimental/windows.yaml, declaring a QEMU-driven
  Windows guest. The image URL is intentionally a 404 placeholder so
  the template is reviewable for its schema shape without misleading
  anyone into thinking `limactl start` is functional yet — the file
  header documents the follow-up work in pkg/cidata and pkg/driver/qemu
  that has to land before boot succeeds.

This is complementary to PR lima-vm#4985 (autounattend route): both routes
need this enum, and pkg/cidata can subsequently branch on guestOS to
emit Cloudbase-Init NoCloud data or autounattend ISOs as the project
decides.

Part of the LFX 2026 Term 2 project "Improve Windows support (host
and guest)" (lima-vm#4907).

Signed-off-by: mn-ram <235066282+mn-ram@users.noreply.github.com>
mn-ram added a commit to mn-ram/lima that referenced this pull request May 16, 2026
…S OS schema, experimental template

Two complementary primary-goal contributions for the LFX 2026 Term 2
project "Improve Windows support (host and guest)" (lima-vm#4907):

1. Windows host UX
   Replaces the cygpath.exe subprocess at pkg/ioutilx/ioutilx.go:54
   with deterministic pure-Go path translation. The implicit Cygwin /
   MSYS2 dependency is removed from Windows hosts. The public
   signature of WindowsSubsystemPath is unchanged, so all eight
   production callers (cmd/limactl/shell.go, pkg/copytool,
   pkg/hostagent/mount, pkg/sshutil x3, pkg/limayaml/defaults)
   continue to work without edits.

   - detectSubsystemStyle() picks one of {native, msys, cygwin} from
     MSYSTEM, CYGWIN, the SSH env var, and the resolved ssh binary
     path. Env vars win over heuristics so a user-asserted MSYSTEM is
     honored.
   - convertWindowsSubsystemPath() does the namespace remapping with
     pure string operations (no path/filepath calls), so the package
     builds and tests on any host. UNC paths pass through with slashes
     normalized, matching cygpath -u's behavior.
   - WindowsSubsystemPathForLinux on line 62 is intentionally left
     alone; it shells out to "wsl --exec wslpath", which is correct
     and not a Cygwin dependency.
   - 23 sub-tests cover style detection, drive-letter and UNC
     conversion, and the public end-to-end entry point. Uses
     gotest.tools/v3/assert per Lima's .golangci.yml.

2. Windows guest scaffolding
   Adds the schema groundwork any Windows-guest provisioning route
   (Cloudbase-Init or autounattend.xml, see lima-vm#4985) needs:

   - WINDOWS OS = "Windows" added to pkg/limatype/lima_yaml.go and
     the OSTypes slice. NewOS() normalizes the lowercase "windows"
     alias the same way it does "linux" and "darwin".
   - pkg/limayaml/validate.go switch on *y.OS whitelists Windows.
   - TestValidateMultipleErrors updated to use "plan9" as the
     rejected OS (was using "windows" as the invalid example, which
     is now valid) and refreshed the expected OSTypes message.
   - TestValidateWindowsGuestOS added — positive + negative cases.
   - templates/experimental/windows.yaml — minimal QEMU-driven
     Windows guest template. Image URL is intentionally a 404
     placeholder so the template is reviewable for its schema shape
     without misleading anyone into thinking limactl start is
     functional yet; the file header documents the follow-up work in
     pkg/cidata and pkg/driver/qemu.

A self-contained Go module under poc/ prototypes the conversion
logic in isolation and is kept in the tree as a runnable demo
(go run ./poc/cmd/winpath-demo 'C:\...'). It is not required by the
production code path and maintainers may strip it before merge.

Out of scope, deferred to follow-up PRs and documented in the
project plan: pkg/cidata branching for Cloudbase-Init data, QEMU
TPM/UEFI plumbing for Windows guests, pkg/driver/hcs external
driver, WSL2 multi-instance + WSLg, limactl doctor for the first-
run Windows wizard.

Signed-off-by: mn-ram <235066282+mn-ram@users.noreply.github.com>
Add dual-architecture (amd64/arm64) unattended Windows installation
using autounattend.xml templates. Includes Joliet ISO generation,
architecture-specific template selection, unit tests, and a standalone
QEMU test script (hack/gen-autounattend-iso.go) for both architectures.

Key design decisions:
- arm64 uses NVMe disk (built-in WinPE driver, no virtio-win dependency)
- amd64 uses AHCI/SATA via q35 (built-in WinPE driver)
- No product keys in either template
- OpenSSH server installed during OOBE for Lima host-agent connectivity

Tested end-to-end:
- Windows 10 Pro x86_64 on QEMU (emulation)
- Windows 11 Pro ARM64 on QEMU+HVF (Apple Silicon, native speed)

Signed-off-by: ashwat287 <ashwatpas@gmail.com>
@ashwat287 ashwat287 force-pushed the autounattendedPOC branch from 528fc97 to f3cd7c9 Compare May 16, 2026 13:36
@ashwat287
Copy link
Copy Markdown
Contributor Author

ashwat287 commented May 16, 2026

since guest arch != host arch, CPU instructions are emulated

Can we support the ARM version of Windows?

Absolutely! I have included the testing section for ARM version in the description. It's much faster, takes 5-10 mins for the boot process to complete. (emulation overhead got dropped + hvf acceleration)

@jandubois
Copy link
Copy Markdown
Member

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants