Install Windows 11 from a disk image with --install-from-img

Reading the installation files through QEMU's emulated USB CD-ROM runs at a
few MB/s on macOS. --install-from-img FILE (or WIN11_VM_INSTALL_IMG) attaches
an installation disk image built by vm-images/build-windows-install-disk.sh as
an NVMe disk instead, which is read about 15 times faster. The image is
attached with snapshot=on because NVMe cannot report write protection and
Windows fails on a read-only volume.

Add windows11-arm64-install-speed.md with the analysis and measurements.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
guofangandClaude Sonnet 5.5 committed 2026-10-17 17:02:58 +08:00
1 parent 946771c6af
commit 1732ec39d0
3 files changed
+205 -7

No files matched your search

+39 -6
View File
@@ -126,14 +126,15 @@ else:
)
daemonize = False
install_from_iso = False
install_img = Path(env("WIN11_VM_INSTALL_IMG")) if env("WIN11_VM_INSTALL_IMG") else None
download_only = False
extra_args = []
def usage(stream=sys.stdout):
stream.write(f"""\
Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--install-from-iso] [--state-dir DIR]
[--rdp-port PORT] [--ssh-port PORT] [-- QEMU_ARGUMENT ...]
Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--install-from-iso | --install-from-img FILE]
[--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.
@@ -141,7 +142,8 @@ The VM hardware (accelerator, memory, CPUs, drives, devices) is defined in
qemu/windows11.ini, which is rendered to qemu.ini in the state directory and
loaded with QEMU's -readconfig, followed by qemu/windows11-<os>-<arch>.ini (os is linux or darwin, arch is amd64 or arm64, chosen from the host system and
architecture 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.
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:
@@ -149,6 +151,12 @@ Options:
--download-only Download the VirtIO tools ISO if it is missing, then exit
(the Windows ISO and base QCOW2 are supplied manually)
--install-from-iso Install from ISO when the system disk is missing
--install-from-img FILE
Install from a disk image made by
vm-images/build-windows-install-disk.sh instead of the ISO
(implies --install-from-iso). It is attached as an NVMe
disk, much faster than the emulated USB CD-ROM, and is
never modified (overrides WIN11_VM_INSTALL_IMG)
--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 forwarded to guest RDP (default: 3389;
@@ -160,6 +168,7 @@ Options:
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
WIN11_VM_INSTALL_IMG Installation disk image used instead of the ISO
WIN11_VM_TOOLS_ISO VirtIO Windows tools ISO (default: resolved by shared image helper)
WIN11_VM_RDP_PORT Host RDP port (default: 3389)
WIN11_VM_SSH_PORT Host SSH port (default: 2222)
@@ -184,6 +193,12 @@ while args:
download_only = True
elif arg == "--install-from-iso":
install_from_iso = True
elif arg == "--install-from-img":
if not args:
die(f"{arg} requires a file argument.", 2)
install_img = Path(args.pop(0))
elif arg.startswith("--install-from-img="):
install_img = Path(arg.split("=", 1)[1])
elif arg == "--state-dir":
if not args:
die(f"{arg} requires a directory argument.", 2)
@@ -213,6 +228,9 @@ while args:
usage(sys.stderr)
sys.exit(2)
if install_img:
install_from_iso = True
if download_only:
# The VirtIO tools ISO is the only medium the launcher downloads itself.
if env("WIN11_VM_TOOLS_ISO"):
@@ -245,8 +263,10 @@ if not disk_image.is_file():
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"]))
# The installation disk image replaces the ISO, which is then not needed.
install_iso = None if install_img else Path(
env("WIN11_VM_INSTALL_ISO")
or capture([str(image_downloader), "--print-path", "windows11", "--iso"]))
uses_shared_tools_iso = False
if env("WIN11_VM_TOOLS_ISO"):
tools_iso = Path(env("WIN11_VM_TOOLS_ISO"))
@@ -262,7 +282,11 @@ if shutil.which(qemu_binary) is None:
if installing:
if shutil.which("qemu-img") is None:
die("qemu-img is not installed.")
if installing_from_iso:
if install_img:
if not install_img.is_file():
die(f"Windows 11 installation disk image was not found: {install_img}\n"
"Build it with vm-images/build-windows-install-disk.sh ISO OUTPUT.img")
elif installing_from_iso:
if not install_iso.is_file():
die(f"Windows 11 ISO was not found: {install_iso}")
elif not base_image.is_file():
@@ -375,6 +399,15 @@ if installing_from_iso:
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_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={install_img},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,