Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions hack/gen-autounattend-iso.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0

// gen-autounattend-iso generates an autounattend.iso for testing the Windows
// guest POC with QEMU.
// Usage: go run hack/gen-autounattend-iso.go [-arch amd64|arm64] [-o autounattend.iso]
package main

import (
"bytes"
"flag"
"fmt"
"os"

"github.com/lima-vm/lima/v2/pkg/cidata"
"github.com/lima-vm/lima/v2/pkg/iso9660util"
)

func main() {
outPath := flag.String("o", "autounattend.iso", "output ISO file path")
arch := flag.String("arch", "amd64", "target architecture: amd64 or arm64")
flag.Parse()

if *arch != "amd64" && *arch != "arm64" {
fmt.Fprintf(os.Stderr, "error: -arch must be amd64 or arm64, got %q\n", *arch)
os.Exit(1)
}

args := &cidata.TemplateArgs{
Name: "windows-test",
}

xmlBytes, err := cidata.ExecuteTemplateAutounattend(args, *arch)
if err != nil {
fmt.Fprintf(os.Stderr, "error rendering template: %v\n", err)
os.Exit(1)
}

// Also write the raw XML for inspection
xmlPath := *outPath + ".xml"
if err := os.WriteFile(xmlPath, xmlBytes, 0o644); err != nil {
fmt.Fprintf(os.Stderr, "error writing XML: %v\n", err)
os.Exit(1)
}
fmt.Fprintf(os.Stdout, "Wrote %s (%d bytes)\n", xmlPath, len(xmlBytes))

layout := []iso9660util.Entry{
{
Path: "autounattend.xml",
Reader: bytes.NewReader(xmlBytes),
},
}

// Only include startup.nsh for amd64 (EFI Shell fallback for OVMF on x86_64)
if *arch == "amd64" {
layout = append(layout, iso9660util.Entry{
Path: "startup.nsh",
Reader: bytes.NewReader([]byte("@echo -off\r\n" +
"echo Searching for Windows Boot Manager...\r\n" +
"FS0:\\EFI\\BOOT\\BOOTX64.EFI\r\n" +
"FS1:\\EFI\\BOOT\\BOOTX64.EFI\r\n" +
"FS2:\\EFI\\BOOT\\BOOTX64.EFI\r\n" +
"FS3:\\EFI\\BOOT\\BOOTX64.EFI\r\n" +
"FS4:\\EFI\\BOOT\\BOOTX64.EFI\r\n")),
})
}

if err := iso9660util.Write(*outPath, "autounattend", layout, iso9660util.WithJoliet()); err != nil {
fmt.Fprintf(os.Stderr, "error writing ISO: %v\n", err)
os.Exit(1)
}

fi, _ := os.Stat(*outPath)
fmt.Fprintf(os.Stdout, "Wrote %s (%d bytes)\n", *outPath, fi.Size())

if *arch == "amd64" {
printAmd64Usage(*outPath)
} else {
printArm64Usage(*outPath)
}
}

func printAmd64Usage(outPath string) {
fmt.Fprintln(os.Stdout, "\nUsage with QEMU amd64 (UEFI boot required):")
fmt.Fprintln(os.Stdout, " qemu-img create -f qcow2 disk.qcow2 64G")
fmt.Fprintln(os.Stdout, " dd if=/dev/zero of=ovmf_vars.fd bs=1M count=4")
fmt.Fprintln(os.Stdout, " qemu-system-x86_64 -m 4G -smp 2 -machine q35 \\")
fmt.Fprintln(os.Stdout, " -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \\")
fmt.Fprintln(os.Stdout, " -drive if=pflash,format=raw,file=ovmf_vars.fd \\")
fmt.Fprintln(os.Stdout, " -cdrom /path/to/windows.iso \\")
fmt.Fprintf(os.Stdout, " -drive file=%s,media=cdrom,index=3 \\\n", outPath)
fmt.Fprintln(os.Stdout, " -drive file=disk.qcow2,index=0,media=disk,format=qcow2 \\")
fmt.Fprintln(os.Stdout, " -nic user,hostfwd=tcp::2222-:22")
fmt.Fprintln(os.Stdout, "\n On macOS (Homebrew): replace OVMF paths with")
fmt.Fprintln(os.Stdout, " /opt/homebrew/share/qemu/edk2-x86_64-code.fd")
fmt.Fprintln(os.Stdout, "\n On Windows (PowerShell, WHPX): add -accel whpx")
}

func printArm64Usage(outPath string) {
fmt.Fprintln(os.Stdout, "\nUsage with QEMU aarch64 on Apple Silicon (HVF):")
fmt.Fprintln(os.Stdout, " qemu-img create -f qcow2 disk.qcow2 64G")
fmt.Fprintln(os.Stdout, " dd if=/dev/zero of=ovmf_vars.fd bs=1M count=64")
fmt.Fprintln(os.Stdout, "")
fmt.Fprintln(os.Stdout, " qemu-system-aarch64 \\")
fmt.Fprintln(os.Stdout, " -machine virt,accel=hvf \\")
fmt.Fprintln(os.Stdout, " -cpu host \\")
fmt.Fprintln(os.Stdout, " -m 4G -smp 4 \\")
fmt.Fprintln(os.Stdout, " -drive if=pflash,format=raw,readonly=on,file=/opt/homebrew/share/qemu/edk2-aarch64-code.fd \\")
fmt.Fprintln(os.Stdout, " -drive if=pflash,format=raw,file=ovmf_vars.fd \\")
fmt.Fprintln(os.Stdout, " -drive file=disk.qcow2,if=none,id=hd0,format=qcow2 \\")
fmt.Fprintln(os.Stdout, " -device nvme,serial=lima0,drive=hd0 \\")
fmt.Fprintln(os.Stdout, " -device qemu-xhci \\")
fmt.Fprintln(os.Stdout, " -device usb-kbd \\")
fmt.Fprintln(os.Stdout, " -device usb-tablet \\")
fmt.Fprintln(os.Stdout, " -drive file=/path/to/Win11_ARM64.iso,if=none,id=cdrom0,media=cdrom,readonly=on \\")
fmt.Fprintln(os.Stdout, " -device usb-storage,drive=cdrom0 \\")
fmt.Fprintf(os.Stdout, " -drive file=%s,if=none,id=unattend,media=cdrom,readonly=on \\\n", outPath)
fmt.Fprintln(os.Stdout, " -device usb-storage,drive=unattend \\")
fmt.Fprintln(os.Stdout, " -device ramfb \\")
fmt.Fprintln(os.Stdout, " -nic user,hostfwd=tcp::2222-:22")
}
233 changes: 233 additions & 0 deletions pkg/cidata/cidata.TEMPLATE.d.Windows.amd64/autounattend.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Windows unattended answer file for Lima guest VMs (amd64).
Ref: https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/update-windows-settings-and-scripts-create-your-own-answer-file-sxs -->
<unattend xmlns="urn:schemas-microsoft-com:unattend">

<!-- windowsPE: locale, disk layout, image selection, hw bypass -->
<settings pass="windowsPE">
<component name="Microsoft-Windows-International-Core-WinPE"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<SetupUILanguage>
<UILanguage>en-US</UILanguage>
</SetupUILanguage>
<InputLocale>en-US</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UserLocale>en-US</UserLocale>
</component>

<component name="Microsoft-Windows-Setup"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">

<!-- Bypass Win11 hardware checks (TPM, SecureBoot, RAM, etc.) — irrelevant in a VM -->
<RunSynchronous>
<RunSynchronousCommand wcm:action="add">
<Order>1</Order>
<Path>reg add "HKLM\SYSTEM\Setup\LabConfig" /v BypassTPMCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>2</Order>
<Path>reg add "HKLM\SYSTEM\Setup\LabConfig" /v BypassSecureBootCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>3</Order>
<Path>reg add "HKLM\SYSTEM\Setup\LabConfig" /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>4</Order>
<Path>reg add "HKLM\SYSTEM\Setup\LabConfig" /v BypassCPUCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>5</Order>
<Path>reg add "HKLM\SYSTEM\Setup\LabConfig" /v BypassStorageCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>6</Order>
<Path>reg add "HKLM\SYSTEM\Setup\MoSetup" /v AllowUpgradesWithUnsupportedTPMOrCPU /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
</RunSynchronous>

<UserData>
<AcceptEula>true</AcceptEula>
<FullName>User</FullName>
<Organization>Lima</Organization>
<ProductKey>
<WillShowUI>Never</WillShowUI>
</ProductKey>
</UserData>

<!-- GPT partition layout for UEFI boot.
NOTE: On amd64, disk is attached via SATA/AHCI so WinPE can see it without extra drivers. -->
<DiskConfiguration>
<Disk wcm:action="add">
<DiskID>0</DiskID>
<WillWipeDisk>true</WillWipeDisk>
<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>
<PartitionID>1</PartitionID>
<Label>System</Label>
<Format>FAT32</Format>
</ModifyPartition>
<ModifyPartition wcm:action="add">
<Order>2</Order>
<PartitionID>2</PartitionID>
</ModifyPartition>
<ModifyPartition wcm:action="add">
<Order>3</Order>
<PartitionID>3</PartitionID>
<Label>Windows</Label>
<Format>NTFS</Format>
<Letter>C</Letter>
</ModifyPartition>
</ModifyPartitions>
</Disk>
</DiskConfiguration>

<!-- Index 6 = Pro on Win10/Win11 x86_64 multi-edition ISOs -->
<ImageInstall>
<OSImage>
<InstallTo>
<DiskID>0</DiskID>
<PartitionID>3</PartitionID>
</InstallTo>
<InstallToAvailablePartition>false</InstallToAvailablePartition>
<OSImageIndex>6</OSImageIndex>
</OSImage>
</ImageInstall>

</component>
</settings>

<!-- specialize: machine identity (runs after file copy, before first boot) -->
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<ComputerName>lima-default</ComputerName>
<TimeZone>UTC</TimeZone>
</component>
</settings>

<!-- oobeSystem: suppress OOBE prompts, create user, install OpenSSH -->
<settings pass="oobeSystem">
<component name="Microsoft-Windows-Shell-Setup"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<OOBE>
<HideEULAPage>true</HideEULAPage>
<HideLocalAccountScreen>false</HideLocalAccountScreen>
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<NetworkLocation>Work</NetworkLocation>
<ProtectYourPC>3</ProtectYourPC>
<SkipMachineOOBE>true</SkipMachineOOBE>
<SkipUserOOBE>true</SkipUserOOBE>
</OOBE>
<UserAccounts>
<LocalAccounts>
<LocalAccount wcm:action="add">
<Name>lima</Name>
<DisplayName>lima</DisplayName>
<Group>Administrators</Group>
<Password>
<Value></Value>
<PlainText>true</PlainText>
</Password>
</LocalAccount>
</LocalAccounts>
</UserAccounts>
<AutoLogon>
<Enabled>true</Enabled>
<Username>lima</Username>
<Password>
<Value></Value>
<PlainText>true</PlainText>
</Password>
<LogonCount>3</LogonCount>
</AutoLogon>

<!-- Install and configure OpenSSH server for Lima host-agent connectivity -->
<FirstLogonCommands>
<SynchronousCommand wcm:action="add">
<Order>1</Order>
<CommandLine>powershell.exe -NoProfile -Command "Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0"</CommandLine>
<Description>Install OpenSSH Server</Description>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>2</Order>
<CommandLine>powershell.exe -NoProfile -Command "Start-Service sshd; Set-Service -Name sshd -StartupType Automatic"</CommandLine>
<Description>Start and enable sshd</Description>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>3</Order>
<CommandLine>powershell.exe -NoProfile -Command "New-Item -Path 'C:\ProgramData\ssh' -ItemType Directory -Force"</CommandLine>
<Description>Create SSH config directory</Description>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>4</Order>
<CommandLine>powershell.exe -NoProfile -Command "Set-Content -Path 'C:\ProgramData\ssh\administrators_authorized_keys' -Value 'ssh-rsa PLACEHOLDER'; icacls 'C:\ProgramData\ssh\administrators_authorized_keys' /inheritance:r /grant 'SYSTEM:(F)' /grant 'Administrators:(F)'"</CommandLine>
<Description>Write authorized_keys with correct ACLs</Description>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>5</Order>
<CommandLine>powershell.exe -NoProfile -Command "$cfg = 'C:\ProgramData\ssh\sshd_config'; (Get-Content $cfg) -replace '#?AuthorizedKeysFile.*', 'AuthorizedKeysFile .ssh/authorized_keys' | Set-Content $cfg; Add-Content $cfg 'Match Group administrators`r`n AuthorizedKeysFile C:/ProgramData/ssh/administrators_authorized_keys'; Restart-Service sshd"</CommandLine>
<Description>Configure sshd for admin key auth</Description>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>6</Order>
<CommandLine>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoAdminLogon /t REG_SZ /d 0 /f</CommandLine>
<Description>Disable auto-logon after provisioning</Description>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>7</Order>
<CommandLine>powershell.exe -NoProfile -Command "New-NetFirewallRule -Name 'OpenSSH-Server' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22"</CommandLine>
<Description>Allow SSH through firewall</Description>
</SynchronousCommand>
</FirstLogonCommands>
</component>
<component name="Microsoft-Windows-International-Core"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<InputLocale>en-US</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
</settings>

</unattend>
Loading
Loading