diff --git a/README.md b/README.md index 9d91fef..7af2cd5 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,7 @@ corresponding `*_STATE_DIR` environment variable: With `--download-only`, a launcher only downloads the media it would fetch automatically and then exits without creating a VM disk or starting QEMU: the -base image for Debian, AlmaLinux, Fedora and Ubuntu, and the VirtIO tools ISO -for Windows 11 (whose installation ISO and base QCOW2 are supplied manually): +base image for Debian, AlmaLinux, Fedora and Ubuntu: ```bash ./run-fedora44.py --download-only @@ -165,43 +164,49 @@ from the shared Windows 11 QCOW2 base image: ./run-windows11.py ``` -It creates a persistent 64G disk at `vm/windows11/disk.qcow2`, automatically -partitions it, completes OOBE, creates the local administrator `guofang` with -the initial password `wang`, and enables Remote Desktop on -`0.0.0.0:3389` (all host interfaces; connect to `localhost:3389` on the host itself). During the first login it also silently installs the VirtIO -drivers (the guest tools installer on amd64, the Arm64 driver packages from the -VirtIO ISO with `pnputil` on arm64), without which the guest has no network on -arm64. The installer includes the VirtIO drivers, SPICE vdagent, and QEMU Guest -Agent. The launcher exposes the vdagent channel for host/guest clipboard -integration and the QEMU Guest Agent at `vm/windows11/qemu-guest-agent.sock`. -Change the password after first login. The OpenSSH Server is installed during -the first logon from the Win32-OpenSSH zip for the host architecture, which the -seed ISO carries (downloaded by `vm-images/download-images.sh openssh-win`), so -Windows Update is not needed. Connect with `ssh guofang@localhost -p 2222`; the -seed ISO also carries the host's SSH public key (`$SSH_PUBLIC_KEY_FILE`, else the -first of `~/.ssh/id_ed25519.pub`, `id_ecdsa.pub`, `id_rsa.pub`) for passwordless -administrator login. +It creates a persistent 64G disk at `vm/windows11/disk.qcow2` as a QCOW2 +overlay on the base image and nothing else: the launcher only creates the +differential disk and starts QEMU. The base image is generic and holds nothing +but Windows (see `vm-images/README.md`): no account, no SSH, no VirtIO or other +hardware-specific drivers, no machine identity. Like a cloud image, each VM is +configured on its first boot, from the seed ISO. + +Whenever the disk does not exist yet, the launcher also mounts the seed ISO +`vm/windows11/seed.iso` (beside the disk; built from `seediso/windows11/` when +missing or outdated, with the OpenSSH zip and the host's SSH public key, +`$SSH_PUBLIC_KEY_FILE` or the first of `~/.ssh/id_ed25519.pub`, `id_ecdsa.pub`, +`id_rsa.pub`). Windows reads the `Unattend.xml` in the root of that CD-ROM +during its specialize and OOBE passes: host name `WIN11`, time zone, Remote +Desktop on `0.0.0.0:3389` (all host interfaces; connect to `localhost:3389` on +the host itself), the local administrator `guofang` (initial password `wang`; +change it after first login) and automatic logon. At the first logon it runs +`Configure-Windows.ps1` from the seed ISO, which installs the OpenSSH Server +from the zip with the SSH key (connect with `ssh guofang@localhost -p 2222`), +turns off Windows Update and copies the numbered installer scripts +(`seediso/windows11/NN-*.bat` and `.ps1`) to the desktop folder `Tools`. Later +starts do not mount the seed ISO. + +Because the image has no VirtIO drivers, the VM only uses devices that Windows +has inbox drivers for: an IDE disk and e1000e NIC on amd64, an NVMe disk, e1000e +NIC and ramfb display on arm64 (a fixed-size framebuffer there). The SPICE +vdagent and QEMU Guest Agent come with the VirtIO guest tools, so host/guest +clipboard integration and the guest agent are not available unless you install +those tools in the VM yourself. The Windows VM hardware (8G memory, 8 CPUs, system disk, devices) is defined in `qemu/windows11/config.ini`, with the accelerators and machine type in `qemu/windows11/-.ini`; the launcher renders the former to `vm/windows11/qemu.ini` and loads both with `-readconfig`. Only the network -(see Port forwards) and the installation media are still set by the launcher. QEMU arguments after +(see Port forwards) and the seed ISO are still set by the launcher. QEMU arguments after `--` override the config files, for example `./run-windows11.py -- -m 4G -smp 4`. -The generated answer-file ISO is stored beside the disk as -`vm/windows11/seed.iso`. -To install from the Windows ISO instead, use `./run-windows11.py ---install-from-iso`. This choice only applies while creating a missing system -disk; subsequent starts always boot the existing system disk normally. During -ISO installation, the launcher resolves its tools ISO through the shared image -helper and downloads it when missing. Override the base QCOW2, source ISO, -tools ISO, RDP port, or SSH port with the `WIN11_VM_` environment -variables shown by `./run-windows11.py --help`. For example, when the VirtIO ISO -is elsewhere: - -```bash -WIN11_VM_TOOLS_ISO=/custom/path/virtio-win.iso ./run-windows11.py --install-from-iso -``` +The launcher has no installation options and stops with a hint if the base image +is missing. Make the base image once with `vm-images/build-windows-base.sh`, +which is self-contained in `vm-images/` (its own answer file, QEMU config and +installation logic, written in bash): it installs Windows unattended from the +ISO into a scratch disk, runs `sysprep` as its last step and stores the disk as +the read-only base image. Override the base QCOW2, seed ISO, RDP port, or SSH +port with the `WIN11_VM_` environment variables shown by +`./run-windows11.py --help`. ## Port forwards diff --git a/qemu/windows11/config.ini b/qemu/windows11/config.ini index 2f9401b..3770c29 100644 --- a/qemu/windows11/config.ini +++ b/qemu/windows11/config.ini @@ -4,8 +4,8 @@ # # run-windows11.py fills in the file path placeholders below, writes the result # to /qemu.ini and passes it to QEMU with -readconfig. Options that -# have no -readconfig equivalent (-cpu, -display) and the installation media -# attached by --install-from-iso are still added by the launcher. +# have no -readconfig equivalent (-cpu, -display) and the network are still added +# by the launcher. [name] guest = "windows11" diff --git a/qemu/windows11/darwin-arm64.ini b/qemu/windows11/darwin-arm64.ini index 86c4c8f..2304733 100644 --- a/qemu/windows11/darwin-arm64.ini +++ b/qemu/windows11/darwin-arm64.ini @@ -7,35 +7,27 @@ [accel] accel = "tcg" -# run-windows11.py adds kernel-irqchip=off unless installing with -# --install-from-img (see there). +# run-windows11.py adds kernel-irqchip=off (see there). [machine] type = "virt" # The "virt" machine has no IDE. NVMe and USB mass storage (used by the -# installation media of --install-from-iso) both have inbox Windows drivers. +# installation media) both have inbox Windows drivers. [device "system_disk-dev"] driver = "nvme" drive = "system_disk" serial = "win11disk" bootindex = "1" -# Windows has no inbox driver for virtio-net; the guest tools installed by -# Configure-Windows.ps1 provide it. +# Windows has no inbox driver for virtio-net or virtio-gpu, and the base image +# contains no VirtIO drivers, so the network card is an Intel e1000e and the +# display is ramfb (a fixed-size framebuffer). [device "nic0"] - driver = "virtio-net-pci" + driver = "e1000e" netdev = "net0" -# Unlike q35, "virt" has no default GPU or keyboard. virtio-gpu is the only -# display, driven by viogpudo (installed by Configure-Windows.ps1). ramfb is not -# added because Windows would then see a second adapter with a fixed-mode -# monitor. xres/yres set the preferred mode in the EDID (QEMU defaults to -# 1280x800); viogpudo accepted 1920x1200, 2048x1280 and 2304x1440 but not -# 2560x1440 or 2560x1600. [device "gpu0"] - driver = "virtio-gpu-pci" - xres = "2304" - yres = "1440" + driver = "ramfb" [device "kbd0"] driver = "usb-kbd" diff --git a/qemu/windows11/linux-amd64.ini b/qemu/windows11/linux-amd64.ini index 7a91e5b..12b477d 100644 --- a/qemu/windows11/linux-amd64.ini +++ b/qemu/windows11/linux-amd64.ini @@ -11,8 +11,7 @@ type = "q35" # The system disk is attached through IDE so that Windows can boot before the -# VirtIO storage driver is installed; the installation media of -# --install-from-iso use ide.1 to ide.3. +# VirtIO storage driver is installed. [device "system_disk-dev"] driver = "ide-hd" drive = "system_disk" diff --git a/qemu/windows11/linux-arm64.ini b/qemu/windows11/linux-arm64.ini index b3925e2..48f828d 100644 --- a/qemu/windows11/linux-arm64.ini +++ b/qemu/windows11/linux-arm64.ini @@ -11,28 +11,22 @@ type = "virt" # The "virt" machine has no IDE. NVMe and USB mass storage (used by the -# installation media of --install-from-iso) both have inbox Windows drivers. +# installation media) both have inbox Windows drivers. [device "system_disk-dev"] driver = "nvme" drive = "system_disk" serial = "win11disk" bootindex = "1" -# Windows has no inbox driver for virtio-net; the guest tools installed by -# Configure-Windows.ps1 provide it. +# Windows has no inbox driver for virtio-net or virtio-gpu, and the base image +# contains no VirtIO drivers, so the network card is an Intel e1000e and the +# display is ramfb (a fixed-size framebuffer). [device "nic0"] - driver = "virtio-net-pci" + driver = "e1000e" netdev = "net0" -# Unlike q35, "virt" has no default GPU or keyboard. virtio-gpu is the only -# display, driven by viogpudo (installed by Configure-Windows.ps1). ramfb is not -# added because Windows would then see a second adapter with a fixed-mode -# monitor. xres/yres set the preferred mode in the EDID (QEMU defaults to -# 1280x800); viogpudo accepted 1920x1200 and 2048x1280 but not 2560x1600. [device "gpu0"] - driver = "virtio-gpu-pci" - xres = "2048" - yres = "1280" + driver = "ramfb" [device "kbd0"] driver = "usb-kbd" diff --git a/run-windows11.py b/run-windows11.py index f693d8c..d2400a7 100755 --- a/run-windows11.py +++ b/run-windows11.py @@ -51,16 +51,6 @@ def find_image_downloader(): return image_downloader -state_dir = Path(env("WIN11_VM_STATE_DIR") or script_dir / "vm" / "windows11") -disk_size = env("WIN11_VM_DISK_SIZE") or "64G" -rdp_port = env("WIN11_VM_RDP_PORT") -ssh_port = env("WIN11_VM_SSH_PORT") -forwards_file = Path( - env("WIN11_VM_FORWARDS") or script_dir / "qemu" / "windows11" / "forwards.conf") -qemu_config_template = Path( - env("WIN11_VM_QEMU_CONFIG") or script_dir / "qemu" / "windows11" / "config.ini") - - def normalize_arch(arch): if arch in ("arm64", "aarch64"): return "arm64" @@ -69,142 +59,6 @@ def normalize_arch(arch): die(f"unsupported architecture: {arch}") -daemonize = False -install_from_iso = False -install_from_img = False -download_only = False -seed_only = False -extra_args = [] -arch_option = None -vnc_listen = None - - -def usage(stream=sys.stdout): - stream.write(f"""\ -Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--seed-only] [--vnc[=ADDR:N]] [--install-from-iso | --install-from-img] - [--arch ARCH] [--state-dir DIR] [--rdp-port PORT] [--ssh-port PORT] [-- QEMU_ARGUMENT ...] - -If the system disk (DIR/disk.qcow2, see --state-dir) is missing, create it from -the Windows 11 QCOW2 base image. -The VM hardware (accelerator, memory, CPUs, drives, devices) is defined in -qemu/windows11/config.ini, which is rendered to qemu.ini in the state directory and -loaded with QEMU's -readconfig, followed by qemu/windows11/-.ini (os is linux or darwin, arch is amd64 or arm64, chosen from the host system and -architecture, --arch or VM_IMAGE_ARCH). Later QEMU arguments override them, e.g. -- -m 4G -smp 4. -Use --install-from-iso to perform an automatic ISO installation instead, or ---install-from-img to install from a disk image built from the ISO. -If it exists, start Windows normally without attaching installation media. - -Options: - --daemon Run QEMU in the background - --download-only Download the VirtIO tools ISO and the Windows 11 ISO - (for --arch) if they are missing, then exit; needs - Node.js and Chromium for the latter, see vm-images/README.md - (the base QCOW2 is supplied manually) - --seed-only Build the seed ISO (answer file and setup scripts) and exit, - without starting the VM or fetching the Windows and VirtIO ISOs - --install-from-iso Install from ISO when the system disk is missing - --install-from-img Like --install-from-iso, but install from the disk image - made by vm-images/build-windows-install-disk.sh, which - is the ISO path ending in .img instead of .iso. - It is attached as an NVMe disk, much faster than the - emulated USB CD-ROM, and is never modified - --vnc[=ADDR:N] Serve the screen over VNC instead of opening a window, on - ADDR (default: 127.0.0.1) at TCP port 5900+N (default N: 0). - Overrides WIN11_VM_DISPLAY - --arch ARCH Guest architecture: arm64 or amd64 (default: the host's; - overrides VM_IMAGE_ARCH). A guest that differs from the - host runs under slow TCG emulation - --state-dir DIR Directory for the VM disk, seed ISO and runtime files - (default: vm/windows11; overrides WIN11_VM_STATE_DIR) - --rdp-port PORT Host port of the RDP rule in the port forward file - (default: 3389; overrides WIN11_VM_RDP_PORT) - --ssh-port PORT Host port of the SSH rule in the port forward file - (default: 2222; overrides WIN11_VM_SSH_PORT) - -h, --help Show this help - -Environment variables: - WIN11_VM_BASE_IMAGE Windows 11 base QCOW2 image (default: beside shared images) - WIN11_VM_INSTALL_ISO Windows 11 installation ISO used with --install-from-iso - (--install-from-img uses the same path ending in .img) - WIN11_VM_TOOLS_ISO VirtIO Windows tools ISO (default: resolved by shared image helper) - WIN11_VM_RDP_PORT Host RDP port (default: from the port forward file, 3389) - WIN11_VM_SSH_PORT Host SSH port (default: from the port forward file, 2222) - WIN11_VM_FORWARDS Port forward file (default: qemu/windows11/forwards.conf) - WIN11_VM_STATE_DIR Runtime directory (default: vm/windows11) - WIN11_VM_DISK_IMAGE System disk path - WIN11_VM_DISK_SIZE New system disk size (default: 64G) - WIN11_VM_SEED_ISO Answer-file ISO path (default: alongside the VM disk) - WIN11_VM_UEFI_CODE OVMF read-only firmware path (auto-detected if unset) - WIN11_VM_UEFI_VARS OVMF variable-store template path (auto-detected if unset) - WIN11_VM_QEMU_CONFIG QEMU config template passed to -readconfig - (default: qemu/windows11/config.ini) - WIN11_VM_DISPLAY QEMU display backend (default: cocoa on macOS, - gtk elsewhere) -""") - - -args = sys.argv[1:] -while args: - arg = args.pop(0) - if arg == "--daemon": - daemonize = True - elif arg == "--download-only": - download_only = True - elif arg == "--seed-only": - seed_only = True - elif arg == "--vnc" or arg.startswith("--vnc="): - vnc_listen = arg.partition("=")[2] or "127.0.0.1:0" - if ":" not in vnc_listen: - vnc_listen += ":0" - elif arg == "--install-from-iso": - install_from_iso = True - elif arg == "--install-from-img": - install_from_img = True - elif arg == "--state-dir": - if not args: - die(f"{arg} requires a directory argument.", 2) - state_dir = Path(args.pop(0)) - elif arg.startswith("--state-dir="): - state_dir = Path(arg.split("=", 1)[1]) - elif arg in ("--rdp-port", "--ssh-port") or arg.startswith(("--rdp-port=", "--ssh-port=")): - name, sep, value = arg.partition("=") - if not sep: - if not args: - die(f"{name} requires a port argument.", 2) - value = args.pop(0) - if not (value.isascii() and value.isdigit() and 1 <= int(value) <= 65535): - die(f"{name} must be a port number from 1 to 65535: {value}", 2) - if name == "--rdp-port": - rdp_port = value - else: - ssh_port = value - elif arg == "--arch": - if not args: - die(f"{arg} requires an architecture argument.", 2) - arch_option = args.pop(0) - elif arg.startswith("--arch="): - arch_option = arg.split("=", 1)[1] - elif arg in ("-h", "--help"): - usage() - sys.exit(0) - elif arg == "--": - extra_args.extend(args) - break - else: - print(f"Error: unknown option: {arg}", file=sys.stderr) - usage(sys.stderr) - sys.exit(2) - -host_arch = normalize_arch(arch_option or env("VM_IMAGE_ARCH") or platform.machine()) -# The shared image helper and build-seed.sh pick the architecture from here. -os.environ["VM_IMAGE_ARCH"] = host_arch -qemu_binary = "qemu-system-aarch64" if host_arch == "arm64" else "qemu-system-x86_64" -host_os = platform.system().lower() -if host_os == "darwin" and host_arch == "amd64": - die("amd64 guests are not supported on macOS hosts; use an arm64 image") -qemu_platform_config = script_dir / "qemu" / "windows11" / f"{host_os}-{host_arch}.ini" - - def pick_firmware(override, *candidates): if override: return Path(override) @@ -214,80 +68,296 @@ def pick_firmware(override, *candidates): return Path(candidates[0]) -if host_arch == "arm64": - uefi_code = pick_firmware( - env("WIN11_VM_UEFI_CODE"), - "/usr/share/AAVMF/AAVMF_CODE.fd", - "/usr/share/edk2/aarch64/QEMU_EFI.fd", - "/opt/homebrew/share/qemu/edk2-aarch64-code.fd", - "/usr/local/share/qemu/edk2-aarch64-code.fd", - ) - uefi_vars_template = pick_firmware( - env("WIN11_VM_UEFI_VARS"), - "/usr/share/AAVMF/AAVMF_VARS.fd", - "/usr/share/edk2/aarch64/vars-template-pflash.raw", - "/opt/homebrew/share/qemu/edk2-arm-vars.fd", - "/usr/local/share/qemu/edk2-arm-vars.fd", - ) -else: - uefi_code = pick_firmware( - env("WIN11_VM_UEFI_CODE"), - "/usr/share/OVMF/OVMF_CODE_4M.fd", - "/usr/share/edk2/ovmf/OVMF_CODE.fd", - "/opt/homebrew/share/qemu/edk2-x86_64-code.fd", - "/usr/local/share/qemu/edk2-x86_64-code.fd", - ) - uefi_vars_template = pick_firmware( - env("WIN11_VM_UEFI_VARS"), - "/usr/share/OVMF/OVMF_VARS_4M.fd", - "/usr/share/edk2/ovmf/OVMF_VARS.fd", - "/opt/homebrew/share/qemu/edk2-i386-vars.fd", - "/usr/local/share/qemu/edk2-i386-vars.fd", - ) -if install_from_img: - install_from_iso = True +class Options: + """Command-line options.""" -if download_only: - # The VirtIO tools ISO and the Windows installation ISO are the media the - # launcher downloads itself; the base QCOW2 is supplied manually. - image_downloader = None - if env("WIN11_VM_TOOLS_ISO"): - tools_iso = Path(env("WIN11_VM_TOOLS_ISO")) - if not tools_iso.is_file(): - die(f"VirtIO Windows tools ISO was not found: {tools_iso}") + def __init__(self): + self.daemonize = False + self.vnc_listen = None + self.state_dir = Path(env("WIN11_VM_STATE_DIR") or script_dir / "vm" / "windows11") + self.rdp_port = env("WIN11_VM_RDP_PORT") + self.ssh_port = env("WIN11_VM_SSH_PORT") + self.arch_option = None + self.extra_args = [] + + +def usage(stream=sys.stdout): + stream.write(f"""\ +Usage: {Path(sys.argv[0]).name} [--daemon] [--vnc[=ADDR:N]] + [--arch ARCH] [--state-dir DIR] [--rdp-port PORT] [--ssh-port PORT] [-- QEMU_ARGUMENT ...] + +If the system disk (DIR/disk.qcow2, see --state-dir) is missing, create it as a +QCOW2 overlay on the Windows 11 base image, which starts in seconds and runs the +specialize and OOBE passes of its answer file on the first boot, and mount the +seed ISO (DIR/seed.iso, built from seediso/windows11 if missing or outdated): +Windows reads its Unattend.xml from the root of that CD-ROM during the specialize +and OOBE passes: host name, account, auto logon, Remote Desktop, then at the +first logon Configure-Windows.ps1 (OpenSSH and the SSH key, settings, installer +scripts on the desktop). If the disk exists, start Windows normally, without the +seed ISO. +Make the base image once with vm-images/build-windows-base.sh; it installs +Windows from the ISO and generalizes the result. The base image contains only +Windows (no account, no SSH, no VirtIO or other hardware-specific drivers), so +the devices in qemu/windows11 are ones Windows has inbox drivers for. +The VM hardware (accelerator, memory, CPUs, drives, devices) is defined in +qemu/windows11/config.ini, which is rendered to qemu.ini in the state directory and +loaded with QEMU's -readconfig, followed by qemu/windows11/-.ini (os is linux or darwin, arch is amd64 or arm64, chosen from the host system and +architecture, --arch or VM_IMAGE_ARCH). Later QEMU arguments override them, e.g. -- -m 4G -smp 4. + +Options: + --daemon Run QEMU in the background + --vnc[=ADDR:N] Serve the screen over VNC instead of opening a window, on + ADDR (default: 127.0.0.1) at TCP port 5900+N (default N: 0). + Overrides WIN11_VM_DISPLAY + --arch ARCH Guest architecture: arm64 or amd64 (default: the host's; + overrides VM_IMAGE_ARCH). A guest that differs from the + host runs under slow TCG emulation + --state-dir DIR Directory for the VM disk and runtime files + (default: vm/windows11; overrides WIN11_VM_STATE_DIR) + --rdp-port PORT Host port of the RDP rule in the port forward file + (default: 3389; overrides WIN11_VM_RDP_PORT) + --ssh-port PORT Host port of the SSH rule in the port forward file + (default: 2222; overrides WIN11_VM_SSH_PORT) + -h, --help Show this help + +Environment variables: + WIN11_VM_BASE_IMAGE Windows 11 base QCOW2 image (default: beside shared images) + WIN11_VM_RDP_PORT Host RDP port (default: from the port forward file, 3389) + WIN11_VM_SSH_PORT Host SSH port (default: from the port forward file, 2222) + WIN11_VM_FORWARDS Port forward file (default: qemu/windows11/forwards.conf) + WIN11_VM_STATE_DIR Runtime directory (default: vm/windows11) + WIN11_VM_DISK_IMAGE System disk path + WIN11_VM_DISK_SIZE New system disk size (default: 64G) + WIN11_VM_SEED_ISO Seed ISO path (default: alongside the VM disk) + WIN11_VM_UEFI_CODE OVMF read-only firmware path (auto-detected if unset) + WIN11_VM_UEFI_VARS OVMF variable-store template path (auto-detected if unset) + WIN11_VM_QEMU_CONFIG QEMU config template passed to -readconfig + (default: qemu/windows11/config.ini) + WIN11_VM_DISPLAY QEMU display backend (default: cocoa on macOS, + gtk elsewhere) +""") + + +def parse_args(args): + """Parse the command line.""" + options = Options() + args = list(args) + while args: + arg = args.pop(0) + if arg == "--daemon": + options.daemonize = True + elif arg == "--vnc" or arg.startswith("--vnc="): + options.vnc_listen = arg.partition("=")[2] or "127.0.0.1:0" + if ":" not in options.vnc_listen: + options.vnc_listen += ":0" + elif arg == "--state-dir": + if not args: + die(f"{arg} requires a directory argument.", 2) + options.state_dir = Path(args.pop(0)) + elif arg.startswith("--state-dir="): + options.state_dir = Path(arg.split("=", 1)[1]) + elif arg in ("--rdp-port", "--ssh-port") or arg.startswith(("--rdp-port=", "--ssh-port=")): + name, sep, value = arg.partition("=") + if not sep: + if not args: + die(f"{name} requires a port argument.", 2) + value = args.pop(0) + if not (value.isascii() and value.isdigit() and 1 <= int(value) <= 65535): + die(f"{name} must be a port number from 1 to 65535: {value}", 2) + if name == "--rdp-port": + options.rdp_port = value + else: + options.ssh_port = value + elif arg == "--arch": + if not args: + die(f"{arg} requires an architecture argument.", 2) + options.arch_option = args.pop(0) + elif arg.startswith("--arch="): + options.arch_option = arg.split("=", 1)[1] + elif arg in ("-h", "--help"): + usage() + sys.exit(0) + elif arg == "--": + options.extra_args.extend(args) + break + else: + print(f"Error: unknown option: {arg}", file=sys.stderr) + usage(sys.stderr) + sys.exit(2) + return options + + +class Platform: + """Guest architecture, host system and the UEFI firmware that fit them.""" + + def __init__(self, arch_option): + self.arch = normalize_arch(arch_option or env("VM_IMAGE_ARCH") or platform.machine()) + # The shared image helper and build-seed.sh pick the architecture from here. + os.environ["VM_IMAGE_ARCH"] = self.arch + self.qemu_binary = "qemu-system-aarch64" if self.arch == "arm64" else "qemu-system-x86_64" + self.os = platform.system().lower() + if self.os == "darwin" and self.arch == "amd64": + die("amd64 guests are not supported on macOS hosts; use an arm64 image") + self.config_template = Path( + env("WIN11_VM_QEMU_CONFIG") or script_dir / "qemu" / "windows11" / "config.ini") + self.platform_config = script_dir / "qemu" / "windows11" / f"{self.os}-{self.arch}.ini" + if self.arch == "arm64": + self.uefi_code = pick_firmware( + env("WIN11_VM_UEFI_CODE"), + "/usr/share/AAVMF/AAVMF_CODE.fd", + "/usr/share/edk2/aarch64/QEMU_EFI.fd", + "/opt/homebrew/share/qemu/edk2-aarch64-code.fd", + "/usr/local/share/qemu/edk2-aarch64-code.fd", + ) + self.uefi_vars_template = pick_firmware( + env("WIN11_VM_UEFI_VARS"), + "/usr/share/AAVMF/AAVMF_VARS.fd", + "/usr/share/edk2/aarch64/vars-template-pflash.raw", + "/opt/homebrew/share/qemu/edk2-arm-vars.fd", + "/usr/local/share/qemu/edk2-arm-vars.fd", + ) + else: + self.uefi_code = pick_firmware( + env("WIN11_VM_UEFI_CODE"), + "/usr/share/OVMF/OVMF_CODE_4M.fd", + "/usr/share/edk2/ovmf/OVMF_CODE.fd", + "/opt/homebrew/share/qemu/edk2-x86_64-code.fd", + "/usr/local/share/qemu/edk2-x86_64-code.fd", + ) + self.uefi_vars_template = pick_firmware( + env("WIN11_VM_UEFI_VARS"), + "/usr/share/OVMF/OVMF_VARS_4M.fd", + "/usr/share/edk2/ovmf/OVMF_VARS.fd", + "/opt/homebrew/share/qemu/edk2-i386-vars.fd", + "/usr/local/share/qemu/edk2-i386-vars.fd", + ) + + +class Runtime: + """Paths of the files a VM keeps in its state directory.""" + + def __init__(self, state_dir): + self.state_dir = state_dir + self.disk_image = Path(env("WIN11_VM_DISK_IMAGE") or state_dir / "disk.qcow2") + self.uefi_vars = state_dir / "OVMF_VARS_4M.fd" + self.pid_file = state_dir / "qemu.pid" + self.log_file = state_dir / "console.log" + self.monitor_socket = state_dir / "qemu-monitor.sock" + self.guest_agent_socket = state_dir / "qemu-guest-agent.sock" + self.qemu_config = state_dir / "qemu.ini" + + +def check_tools_and_firmware(plat, need_qemu_img=True): + if shutil.which(plat.qemu_binary) is None: + die(f"{plat.qemu_binary} is not installed.") + if need_qemu_img and shutil.which("qemu-img") is None: + die("qemu-img is not installed.") + for firmware_file in (plat.uefi_code, plat.uefi_vars_template): + if not firmware_file.is_file(): + die(f"required UEFI firmware was not found: {firmware_file}") + + +def prepare_runtime(plat, rt): + """Create the state directory and clear the leftovers of an earlier run.""" + rt.state_dir.mkdir(parents=True, exist_ok=True) + if rt.pid_file.is_file(): + old_pid = rt.pid_file.read_text().strip() + if old_pid.isdigit(): + try: + os.kill(int(old_pid), 0) + except OSError: + pass + else: + die(f"Windows 11 VM is already running (PID {old_pid}).") + rt.pid_file.unlink(missing_ok=True) + rt.monitor_socket.unlink(missing_ok=True) + rt.guest_agent_socket.unlink(missing_ok=True) + if not rt.uefi_vars.is_file(): + shutil.copyfile(plat.uefi_vars_template, rt.uefi_vars) + + +def display_backend(vnc_listen): + """cocoa shows the guest at its own resolution. Append ,zoom-to-fit=on (via + WIN11_VM_DISPLAY) to stretch a low-resolution screen to the window.""" + backend = (f"vnc={vnc_listen}" if vnc_listen else env("WIN11_VM_DISPLAY") or ( + "cocoa" if platform.system() == "Darwin" else "gtk")) + if vnc_listen: + vnc_host, _, vnc_display = vnc_listen.rpartition(":") + if not vnc_display.isdigit(): + die(f"--vnc display number must be an integer: {vnc_listen}", 2) + print(f"VNC: connect to {vnc_host or '127.0.0.1'}:{5900 + int(vnc_display)}") + return backend + + +def common_qemu_args(plat, rt, options): + """Render the config template and return the QEMU arguments shared by both + launchers, plus the port forwards (for the start-up message). + + Most of the VM definition lives in the QEMU config template; the file + paths are filled in and the rendered file is handed to QEMU with -readconfig. + """ + config_values = { + "uefi_code": plat.uefi_code, + "uefi_vars": rt.uefi_vars, + "disk_image": rt.disk_image, + "guest_agent_socket": rt.guest_agent_socket, + "monitor_socket": rt.monitor_socket, + } + for key, value in config_values.items(): + # QEMU's config file parser has no escape syntax for quoted values. + if any(char in str(value) for char in '"\n'): + die(f"{key} must not contain double quotes or newlines: {value}") + for config_file in (plat.config_template, plat.platform_config): + if not config_file.is_file(): + die(f"QEMU config file was not found: {config_file}") + try: + config_text = string.Template(plat.config_template.read_text()).substitute( + {key: str(value) for key, value in config_values.items()}) + except (KeyError, ValueError) as error: + die(f"invalid placeholder in {plat.config_template}: {error}") + rt.qemu_config.write_text(config_text) + + # Each rule of the port forward file is one hostfwd entry of the network. + forwards_file = Path( + env("WIN11_VM_FORWARDS") or script_dir / "qemu" / "windows11" / "forwards.conf") + forwards = vmforwards.usable_forwards(vmforwards.load_forwards( + forwards_file, + {label: port for label, port in (("RDP", options.rdp_port), ("SSH", options.ssh_port)) + if port})) + + qemu_args = [ + "-readconfig", str(rt.qemu_config), + "-readconfig", str(plat.platform_config), + # "max" is the host CPU under KVM/HVF and the fullest model under TCG. + "-cpu", "max", + # A config file section cannot hold two hostfwd entries, so the network + # that the NIC in the config file uses is defined here. -nic none stops + # QEMU from adding its default NIC as well. + "-netdev", vmforwards.netdev_argument(forwards), + "-nic", "none", + "-display", display_backend(options.vnc_listen), + ] + return qemu_args, forwards + + +def start_qemu(plat, rt, qemu_args, extra_args, forwards, daemonize, banner): + print(banner) + print("Account: guofang / password; " + "; ".join( + f"{forward.label}: 0.0.0.0:{forward.host_port}" for forward in forwards), + flush=True) + if daemonize: + rt.log_file.write_text("") + qemu_args += ["-serial", f"file:{rt.log_file}", "-daemonize", "-pidfile", str(rt.pid_file)] + run([plat.qemu_binary, *qemu_args, *extra_args]) + print(f"VM started (PID {rt.pid_file.read_text().strip()}); console log: {rt.log_file}") else: - image_downloader = find_image_downloader() - tools_iso = Path(capture([str(image_downloader), "--print-path", "virtio-win"])) - if not tools_iso.is_file(): - run([str(image_downloader), "virtio-win"]) - if not tools_iso.is_file(): - die(f"downloader did not create: {tools_iso}") - print(f"VirtIO Windows tools ISO is ready: {tools_iso}") - if env("WIN11_VM_INSTALL_ISO"): - install_iso = Path(env("WIN11_VM_INSTALL_ISO")) - if not install_iso.is_file(): - die(f"Windows 11 installation ISO was not found: {install_iso}") - else: - image_downloader = image_downloader or find_image_downloader() - run([str(image_downloader), "windows11"]) - install_iso = Path(capture([str(image_downloader), "--print-path", "windows11", "--iso"])) - print(f"Windows 11 installation ISO is ready: {install_iso}") - sys.exit(0) - -disk_image = Path(env("WIN11_VM_DISK_IMAGE") or state_dir / "disk.qcow2") -seed_iso = Path(env("WIN11_VM_SEED_ISO") or disk_image.parent / "seed.iso") -uefi_vars = state_dir / "OVMF_VARS_4M.fd" -pid_file = state_dir / "qemu.pid" -log_file = state_dir / "console.log" -monitor_socket = state_dir / "qemu-monitor.sock" -guest_agent_socket = state_dir / "qemu-guest-agent.sock" -qemu_config = state_dir / "qemu.ini" + os.execvp(plat.qemu_binary, [plat.qemu_binary, *qemu_args, *extra_args]) -def build_seed_iso(image_downloader, force=False): - """Build the seed ISO, unless it is newer than all of its sources and not forced.""" +def build_seed_iso(seed_iso): + """Build the seed ISO, unless it is newer than all of its sources.""" seed_dir = script_dir / "seediso" / "windows11" - seed_sources = [seed_dir / "Autounattend.xml", seed_dir / "Configure-Windows.ps1", + seed_sources = [seed_dir / "Unattend.xml", seed_dir / "Configure-Windows.ps1", *sorted(seed_dir.glob("[0-9][0-9]-*.bat")), *sorted(seed_dir.glob("[0-9][0-9]-*.ps1"))] @@ -300,222 +370,49 @@ def build_seed_iso(image_downloader, force=False): # The seed carries the OpenSSH zip for this architecture; build-seed.sh # downloads it when missing. - seed_sources.append(Path(capture( - [str(image_downloader), "--print-path", "openssh-win"]))) + openssh_zip = Path(capture( + [str(find_image_downloader()), "--print-path", "openssh-win"])) + seed_sources.append(openssh_zip) - def newer_than_seed(source): - # Mirrors bash `-nt`: true if source exists and is newer than seed_iso. - return source.exists() and source.stat().st_mtime > seed_iso.stat().st_mtime - - if force or not seed_iso.is_file() or not seed_sources[-1].is_file() \ - or any(newer_than_seed(s) for s in seed_sources): - run([str(script_dir / "seediso" / "windows11" / "build-seed.sh"), str(seed_iso), host_arch]) + if not seed_iso.is_file() or not openssh_zip.is_file() or any( + source.exists() and source.stat().st_mtime > seed_iso.stat().st_mtime + for source in seed_sources): + run([str(seed_dir / "build-seed.sh"), str(seed_iso), plat.arch]) -if seed_only: - build_seed_iso(find_image_downloader(), force=True) - print(f"Seed ISO is ready: {seed_iso}") - sys.exit(0) +options = parse_args(sys.argv[1:]) +plat = Platform(options.arch_option) +rt = Runtime(options.state_dir) +disk_size = env("WIN11_VM_DISK_SIZE") or "64G" -installing = False -installing_from_iso = False -if not disk_image.is_file(): - installing = True - installing_from_iso = install_from_iso - image_downloader = find_image_downloader() - if install_from_iso: - install_iso = Path( - env("WIN11_VM_INSTALL_ISO") - or capture([str(image_downloader), "--print-path", "windows11", "--iso"])) - if install_from_img: - install_iso = install_iso.with_suffix(".img") - uses_shared_tools_iso = False - if env("WIN11_VM_TOOLS_ISO"): - tools_iso = Path(env("WIN11_VM_TOOLS_ISO")) - else: - tools_iso = Path(capture([str(image_downloader), "--print-path", "virtio-win"])) - uses_shared_tools_iso = True - else: - base_image = Path(env("WIN11_VM_BASE_IMAGE") - or capture([str(image_downloader), "--print-path", "windows11"])) +creating = not rt.disk_image.is_file() +if creating: + base_image = Path(env("WIN11_VM_BASE_IMAGE") + or capture([str(find_image_downloader()), "--print-path", "windows11"])) + if not base_image.is_file(): + die(f"Windows 11 base QCOW2 image was not found: {base_image}\n" + "Make it once with vm-images/build-windows-base.sh") -if shutil.which(qemu_binary) is None: - die(f"{qemu_binary} is not installed.") -if installing: - if shutil.which("qemu-img") is None: - die("qemu-img is not installed.") - if installing_from_iso: - if not install_iso.is_file(): - if install_from_img: - die(f"Windows 11 installation disk image was not found: {install_iso}\n" - "Build it with vm-images/build-windows-install-disk.sh ISO") - die(f"Windows 11 ISO was not found: {install_iso}") - elif not base_image.is_file(): - die(f"Windows 11 base QCOW2 image was not found: {base_image}") -for firmware_file in (uefi_code, uefi_vars_template): - if not firmware_file.is_file(): - die(f"required UEFI firmware was not found: {firmware_file}") +check_tools_and_firmware(plat, need_qemu_img=creating) +prepare_runtime(plat, rt) +if creating: + seed_iso = Path(env("WIN11_VM_SEED_ISO") or rt.disk_image.parent / "seed.iso") + build_seed_iso(seed_iso) +if creating: + run(["qemu-img", "create", "-q", "-f", "qcow2", "-F", "qcow2", + "-b", str(base_image), str(rt.disk_image), disk_size]) + print(f"Created Windows 11 system disk: {rt.disk_image}") -state_dir.mkdir(parents=True, exist_ok=True) -if installing_from_iso: - if not tools_iso.is_file(): - if uses_shared_tools_iso: - run([str(image_downloader), "virtio-win"]) - else: - die(f"VirtIO Windows tools ISO was not found: {tools_iso}") - if not tools_iso.is_file(): - die(f"downloader did not create: {tools_iso}") - - build_seed_iso(image_downloader) - -if pid_file.is_file(): - old_pid = pid_file.read_text().strip() - if old_pid.isdigit(): - try: - os.kill(int(old_pid), 0) - except OSError: - pass - else: - die(f"Windows 11 VM is already running (PID {old_pid}).") - pid_file.unlink(missing_ok=True) -monitor_socket.unlink(missing_ok=True) -guest_agent_socket.unlink(missing_ok=True) -if not uefi_vars.is_file(): - shutil.copyfile(uefi_vars_template, uefi_vars) -if installing: - if installing_from_iso: - run(["qemu-img", "create", "-q", "-f", "qcow2", str(disk_image), disk_size]) - else: - run(["qemu-img", "create", "-q", "-f", "qcow2", "-F", "qcow2", - "-b", str(base_image), str(disk_image), disk_size]) - print(f"Created Windows 11 system disk: {disk_image}") - -# cocoa shows the guest at its own resolution. Append ,zoom-to-fit=on (via -# WIN11_VM_DISPLAY) to stretch a low-resolution screen to the window. -display_backend = (f"vnc={vnc_listen}" if vnc_listen else env("WIN11_VM_DISPLAY") or ( - "cocoa" if platform.system() == "Darwin" else "gtk")) -if vnc_listen: - vnc_host, _, vnc_display = vnc_listen.rpartition(":") - if not vnc_display.isdigit(): - die(f"--vnc display number must be an integer: {vnc_listen}", 2) - print(f"VNC: connect to {vnc_host or '127.0.0.1'}:{5900 + int(vnc_display)}") - -# Most of the VM definition lives in the QEMU config template; fill in the -# file paths and hand the rendered file to QEMU with -readconfig. -config_values = { - "uefi_code": uefi_code, - "uefi_vars": uefi_vars, - "disk_image": disk_image, - "guest_agent_socket": guest_agent_socket, - "monitor_socket": monitor_socket, -} -for key, value in config_values.items(): - # QEMU's config file parser has no escape syntax for quoted values. - if any(char in str(value) for char in '"\n'): - die(f"{key} must not contain double quotes or newlines: {value}") -for config_file in (qemu_config_template, qemu_platform_config): - if not config_file.is_file(): - die(f"QEMU config file was not found: {config_file}") -try: - qemu_config_text = string.Template(qemu_config_template.read_text()).substitute( - {key: str(value) for key, value in config_values.items()}) -except (KeyError, ValueError) as error: - die(f"invalid placeholder in {qemu_config_template}: {error}") -qemu_config.write_text(qemu_config_text) - -# Each rule of the port forward file is one hostfwd entry of the network. -forwards = vmforwards.usable_forwards(vmforwards.load_forwards( - forwards_file, {label: port for label, port in (("RDP", rdp_port), ("SSH", ssh_port)) if port})) - -# Neither Windows Setup nor the base image has the viogpudo driver yet, and -# without it the virtio-gpu in the config file shows "display output is not -# active". ramfb shows the screen until Configure-Windows.ps1 installs it. It -# must be created before the virtio-gpu: the display window shows the first GPU. -qemu_args = ["-device", "ramfb"] if installing and host_arch == "arm64" else [] -qemu_args += [ - "-readconfig", str(qemu_config), - "-readconfig", str(qemu_platform_config), - # "max" is the host CPU under KVM/HVF and the fullest model under TCG. - "-cpu", "max", - # A config file section cannot hold two hostfwd entries, so the network - # that the NIC in the config file uses is defined here. -nic none stops - # QEMU from adding its default NIC as well. - "-netdev", vmforwards.netdev_argument(forwards), - "-nic", "none", - "-display", display_backend, - # "-display", "none", "-spice", "port=5900,addr=127.0.0.1,disable-ticketing=on,agent-mouse=off", "-device", "virtio-vga", -] -if host_os == "darwin" and host_arch == "arm64" and not install_from_img: - # With the HVF in-kernel GICv3, QEMU's usb-storage races on the status - # packet and Windows Setup stalls 10 s after every burst of reads - # (utmapp/UTM#7970), so use the emulated interrupt controller; upstream - # QEMU only gets the fix from the UTM fork. The NVMe install disk of - # --install-from-img does not use usb-storage, and hangs with the emulated - # controller. +qemu_args, forwards = common_qemu_args(plat, rt, options) +if plat.os == "darwin" and plat.arch == "arm64": + # The HVF in-kernel GICv3 stalls Windows on its USB and NVMe devices + # (utmapp/UTM#7970), so use the emulated interrupt controller. qemu_args += ["-machine", "kernel-irqchip=off"] -if installing_from_iso: - # q35 has IDE for the installation media; "virt" does not, so use USB. - for media_id, media_file, boot_index, ide_bus in ( - ("install_media", install_iso, 2, "ide.1"), - ("answer_media", seed_iso, 3, "ide.2"), - ("tools_media", tools_iso, 4, "ide.3")): - device = (f"ide-cd,drive={media_id},bus={ide_bus},bootindex={boot_index}" - if host_arch == "amd64" - else f"usb-storage,drive={media_id},bootindex={boot_index}") - if media_id == "install_media" and install_from_img: - # snapshot=on: NVMe cannot report write protection, and Windows - # writes to the volume it mounts, so a read-only drive makes it fail. - # The writes go to a temporary overlay and the image stays unchanged. - qemu_args += [ - "-drive", f"file={media_file},if=none,id={media_id},format=raw,snapshot=on", - "-device", f"nvme,drive={media_id},serial=installer,bootindex={boot_index}", - ] - continue - qemu_args += [ - "-drive", f"file={media_file},if=none,id={media_id},media=cdrom,readonly=on", - "-device", device, - ] - -if installing_from_iso: - print("Starting Windows 11 automatic installation.") -elif installing: - print("Starting Windows 11 from the QCOW2 base image.") -else: - print("Starting Windows 11 normally.") -print("Account: guofang / password; " + "; ".join( - f"{forward.label}: 0.0.0.0:{forward.host_port}" for forward in forwards), - flush=True) - -if installing_from_iso and not install_from_img: - # The Windows ISO boots through bootmgr, which waits for a key ("Press any - # key to boot from CD or DVD...") and gives up after a few seconds. Nobody - # is there to press it, so a detached helper taps the space bar through the - # QEMU monitor for the first 30 s. It exits by itself; later key presses - # are harmless because Setup is unattended. - key_script = ( - "import socket, sys, time\n" - "path = sys.argv[1]\n" - "end = time.time() + 30\n" - "while time.time() < end:\n" - " try:\n" - " s = socket.socket(socket.AF_UNIX)\n" - " s.connect(path)\n" - " break\n" - " except OSError:\n" - " time.sleep(0.5)\n" - "else:\n" - " sys.exit(0)\n" - "while time.time() < end:\n" - " s.sendall(b'sendkey spc\\n')\n" - " time.sleep(1)\n") - subprocess.Popen([sys.executable, "-c", key_script, str(monitor_socket)], - stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, start_new_session=True) - -if daemonize: - log_file.write_text("") - qemu_args += ["-serial", f"file:{log_file}", "-daemonize", "-pidfile", str(pid_file)] - run([qemu_binary, *qemu_args, *extra_args]) - print(f"VM started (PID {pid_file.read_text().strip()}); console log: {log_file}") -else: - os.execvp(qemu_binary, [qemu_binary, *qemu_args, *extra_args]) +if creating: + # q35 has IDE (ide.0 is the system disk); "virt" does not, so use USB. + qemu_args += ["-drive", f"file={seed_iso},if=none,id=seed_media,media=cdrom,readonly=on", + "-device", "ide-cd,drive=seed_media,bus=ide.1" if plat.arch == "amd64" + else "usb-storage,drive=seed_media"] +start_qemu(plat, rt, qemu_args, options.extra_args, forwards, options.daemonize, + "Starting Windows 11 from the QCOW2 base image." if creating + else "Starting Windows 11 normally.") diff --git a/seediso/windows11/Autounattend.xml b/seediso/windows11/Autounattend.xml deleted file mode 100644 index 7cff78e..0000000 --- a/seediso/windows11/Autounattend.xml +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - en-US - - en-US - en-US - en-US - en-US - - - - false - Never - - - - 0 - true - - - 1 - EFI - 100 - - - 2 - MSR - 16 - - - 3 - Primary - true - - - - - 1 - 1 - FAT32 - - - - 2 - 3 - NTFS - - C - - - - OnError - - - - - - /IMAGE/INDEX - 6 - - - - 0 - 3 - - OnError - - - - true - - W269N-WFGWX-YVC9B-4J6C9-T83GX - Never - - - - - 1 - reg add HKLM\SYSTEM\Setup\LabConfig /v BypassTPMCheck /t REG_DWORD /d 1 /f - - - 2 - reg add HKLM\SYSTEM\Setup\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 1 /f - - - 3 - reg add HKLM\SYSTEM\Setup\LabConfig /v BypassRAMCheck /t REG_DWORD /d 1 /f - - - - - - - WIN11 - China Standard Time - - - false - - - - - true - Remote Desktop - all - - - - - - - en-US - en-US - en-US - en-US - - - - true - true - true - 3 - - - - - guofang - Administrators - Guofang - - wang - true</PlainText> - </Password> - </LocalAccount> - </LocalAccounts> - </UserAccounts> - <AutoLogon> - <Enabled>true</Enabled> - <LogonCount>1</LogonCount> - <Username>guofang</Username> - <Password> - <Value>wang</Value> - <PlainText>true</PlainText> - </Password> - </AutoLogon> - <FirstLogonCommands> - <SynchronousCommand wcm:action="add"> - <Order>1</Order> - <Description>Configure Windows from the answer-media script</Description> - <CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "&amp; ('{0}:\Configure-Windows.ps1' -f (Get-Volume -FileSystemLabel CIDATA).DriveLetter) -InstallVirtIO -EnableAutomaticLogon -DisableAutoUpdate -CopyInstallers -InstallOpenSSH -NoPause"</CommandLine> - <RequiresUserInput>false</RequiresUserInput> - </SynchronousCommand> - </FirstLogonCommands> - </component> - </settings> -</unattend> diff --git a/seediso/windows11/Configure-Windows.ps1 b/seediso/windows11/Configure-Windows.ps1 index d980d79..094483a 100644 --- a/seediso/windows11/Configure-Windows.ps1 +++ b/seediso/windows11/Configure-Windows.ps1 @@ -1,5 +1,4 @@ param( - [switch]$InstallVirtIO, [switch]$EnableAutomaticLogon, [switch]$DisableAutoUpdate, [switch]$CopyInstallers, @@ -35,72 +34,6 @@ if (-not (Test-Administrator)) { exit $elevated.ExitCode } -# The guest tools installer is an x86 program and the VirtIO ISO has no Arm64 -# package for it, so on Arm64 Windows the Arm64 drivers are installed from their -# folders on the ISO instead. NetKVM is the network driver; without it the guest -# has no network. -function Install-VirtIOArm64Drivers { - $roots = (Get-PSDrive -PSProvider FileSystem).Root - $drivers = @( - @{ Folder = 'NetKVM'; Inf = 'netkvm.inf' }, - @{ Folder = 'vioserial'; Inf = 'vioser.inf' }, - @{ Folder = 'Balloon'; Inf = 'balloon.inf' }, - @{ Folder = 'viorng'; Inf = 'viorng.inf' }, - @{ Folder = 'vioinput'; Inf = 'vioinput.inf' }, - @{ Folder = 'viogpudo'; Inf = 'viogpudo.inf' } - ) - - $installed = 0 - foreach ($driver in $drivers) { - $inf = $roots | - ForEach-Object { Join-Path $_ "$($driver.Folder)\w11\ARM64\$($driver.Inf)" } | - Where-Object { Test-Path -LiteralPath $_ } | - Select-Object -First 1 - if (-not $inf) { - if ($driver.Folder -eq 'NetKVM') { - throw 'The Arm64 NetKVM driver was not found on the attached media.' - } - continue - } - - & pnputil.exe /add-driver $inf /install | Out-Host - # 3010 asks for a restart; 259 means that no device uses the driver. - if ($LASTEXITCODE -notin 0, 259, 3010) { - throw "pnputil failed to install $inf (exit code $LASTEXITCODE)." - } - $installed++ - } - Write-Host "Installed $installed VirtIO Arm64 driver packages." -} - -function Install-VirtIOGuestTools { - if ([Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { - Install-VirtIOArm64Drivers - return - } - - $installer = Get-ChildItem ` - -Path (Get-PSDrive -PSProvider FileSystem).Root ` - -Filter 'virtio-win-guest-tools.exe' ` - -File ` - -ErrorAction SilentlyContinue | - Select-Object -First 1 - - if (-not $installer) { - throw 'virtio-win-guest-tools.exe was not found on the attached media.' - } - - $process = Start-Process ` - -FilePath $installer.FullName ` - -ArgumentList '/quiet', '/norestart' ` - -Wait ` - -PassThru - - if ($process.ExitCode -notin 0, 3010) { - throw "VirtIO guest tools installer exited with code $($process.ExitCode)." - } -} - function Enable-AutomaticLogon { $winlogonPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' @@ -205,9 +138,6 @@ function Install-OpenSSHServer { $exitCode = 0 try { - if ($InstallVirtIO) { - Install-VirtIOGuestTools - } if ($EnableAutomaticLogon) { Enable-AutomaticLogon } diff --git a/seediso/windows11/Unattend.xml b/seediso/windows11/Unattend.xml new file mode 100644 index 0000000..4f8b52c --- /dev/null +++ b/seediso/windows11/Unattend.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="utf-8"?> +<unattend xmlns="urn:schemas-microsoft-com:unattend"> + <settings pass="specialize"> + <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" + publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <ComputerName>WIN11</ComputerName> + <TimeZone>China Standard Time</TimeZone> + </component> + <component name="Microsoft-Windows-TerminalServices-LocalSessionManager" + processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" + versionScope="nonSxS"> + <fDenyTSConnections>false</fDenyTSConnections> + </component> + <component name="Networking-MPSSVC-Svc" processorArchitecture="amd64" + publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <FirewallGroups> + <FirewallGroup xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" + wcm:action="add" wcm:keyValue="RemoteDesktop"> + <Active>true</Active> + <Group>Remote Desktop</Group> + <Profile>all</Profile> + </FirewallGroup> + </FirewallGroups> + </component> + </settings> + <settings pass="oobeSystem"> + <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" + publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <InputLocale>en-US</InputLocale> + <SystemLocale>en-US</SystemLocale> + <UILanguage>en-US</UILanguage> + <UserLocale>en-US</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"> + <OOBE> + <HideEULAPage>true</HideEULAPage> + <HideOnlineAccountScreens>true</HideOnlineAccountScreens> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <ProtectYourPC>3</ProtectYourPC> + </OOBE> + <UserAccounts> + <LocalAccounts> + <LocalAccount wcm:action="add"> + <Name>guofang</Name> + <Group>Administrators</Group> + <DisplayName>Guofang</DisplayName> + <Password> + <Value>wang</Value> + <PlainText>true</PlainText> + </Password> + </LocalAccount> + </LocalAccounts> + </UserAccounts> + <AutoLogon> + <Enabled>true</Enabled> + <LogonCount>1</LogonCount> + <Username>guofang</Username> + <Password> + <Value>wang</Value> + <PlainText>true</PlainText> + </Password> + </AutoLogon> + <FirstLogonCommands> + <SynchronousCommand wcm:action="add"> + <Order>1</Order> + <Description>Configure Windows from the answer-media script</Description> + <CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "&amp; ('{0}:\Configure-Windows.ps1' -f (Get-Volume -FileSystemLabel CIDATA).DriveLetter) -EnableAutomaticLogon -DisableAutoUpdate -CopyInstallers -InstallOpenSSH -NoPause"</CommandLine> + <RequiresUserInput>false</RequiresUserInput> + </SynchronousCommand> + </FirstLogonCommands> + </component> + </settings> +</unattend> diff --git a/seediso/windows11/build-seed.sh b/seediso/windows11/build-seed.sh index 6b7535d..421b1b9 100755 --- a/seediso/windows11/build-seed.sh +++ b/seediso/windows11/build-seed.sh @@ -32,10 +32,10 @@ fi work_dir="$(mktemp -d)" trap 'rm -rf "$work_dir"' EXIT sed "s/processorArchitecture=\"amd64\"/processorArchitecture=\"${answer_arch}\"/" \ - "$script_dir/Autounattend.xml" >"$work_dir/Autounattend.xml" + "$script_dir/Unattend.xml" >"$work_dir/Unattend.xml" seed_files=( - "$work_dir/Autounattend.xml" + "$work_dir/Unattend.xml" "$script_dir/Configure-Windows.ps1" "$openssh_zip" ) diff --git a/vm-images b/vm-images index 1dc4065..cb01d2c 160000 --- a/vm-images +++ b/vm-images @@ -1 +1 @@ -Subproject commit 1dc40655dffcc07ca34907b6d36063d8dbe7d279 +Subproject commit cb01d2c1207c08fe30a157ab8ceebe4388c7f2e6