build-windows-base.sh: show a window by default, fix macOS bash 3.2 and fork crashes

Replace --window with --headless (VNC); the window is the default. Fill the
QEMU config placeholders without nested quotes (bash 3.2 kept them literally),
and run QEMU as a background job instead of -daemonize when it shows a window
(Cocoa crashes in fork()).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
guofangandClaude Sonnet 5.5 committed 2026-11-02 21:31:10 +08:00
1 parent 4eb26e3a96
commit 91ae8cb147
2 files changed
+27 -12

No files matched your search

+1 -1
View File
@@ -130,7 +130,7 @@ It needs `qemu`, `qemu-img`, an ISO tool (`xorriso`, `genisoimage` or `mkisofs`)
and `socat` or `nc`; there is no SSH to the guest. and `socat` or `nc`; there is no SSH to the guest.
```bash ```bash
./build-windows-base.sh [--force] [--compress] [--from-img] [--window] [OUTPUT.qcow2] ./build-windows-base.sh [--force] [--compress] [--from-img] [--headless] [OUTPUT.qcow2]
``` ```
It refuses to replace an existing base image without `--force`, because the It refuses to replace an existing base image without `--force`, because the
+26 -11
View File
@@ -24,7 +24,7 @@
# install-iso/ (see README.md). # install-iso/ (see README.md).
# #
# Usage: build-windows-base.sh [--force] [--keep-work] [--compress] [--from-img] # Usage: build-windows-base.sh [--force] [--keep-work] [--compress] [--from-img]
# [--window] [OUTPUT.qcow2] # [--headless] [OUTPUT.qcow2]
# OUTPUT defaults to the path that "download-images.sh --print-path windows11" # OUTPUT defaults to the path that "download-images.sh --print-path windows11"
# prints. # prints.
set -euo pipefail set -euo pipefail
@@ -38,7 +38,7 @@ win_dir="${images_dir}/windows11"
usage() { usage() {
cat <<EOF cat <<EOF
Usage: $(basename "$0") [--force] [--keep-work] [--compress] [--from-img] Usage: $(basename "$0") [--force] [--keep-work] [--compress] [--from-img]
[--window] [OUTPUT.qcow2] [--headless] [OUTPUT.qcow2]
Install Windows 11 from the ISO, generalize it with sysprep and store the disk Install Windows 11 from the ISO, generalize it with sysprep and store the disk
as the read-only, generic base image that VM disks are cloned from. as the read-only, generic base image that VM disks are cloned from.
@@ -49,8 +49,9 @@ as the read-only, generic base image that VM disks are cloned from.
--from-img Install from the disk image made by build-windows-install-disk.sh --from-img Install from the disk image made by build-windows-install-disk.sh
(the ISO path ending in .img), which reads much faster than (the ISO path ending in .img), which reads much faster than
the emulated CD-ROM the emulated CD-ROM
--window Show the guest in a QEMU window (needs a desktop session) --headless Serve the guest screen over VNC on 127.0.0.1:5990 instead of
instead of the VNC display on 127.0.0.1:5990 showing a QEMU window (the default; without a desktop
session on Linux, VNC is used as well)
-h, --help Show this help -h, --help Show this help
Environment variables: Environment variables:
@@ -60,7 +61,7 @@ Environment variables:
WIN11_VM_DISK_SIZE Size of the system disk (default: 64G) WIN11_VM_DISK_SIZE Size of the system disk (default: 64G)
WIN11_VM_UEFI_CODE OVMF read-only firmware (auto-detected if unset) WIN11_VM_UEFI_CODE OVMF read-only firmware (auto-detected if unset)
WIN11_VM_UEFI_VARS OVMF variable-store template (auto-detected if unset) WIN11_VM_UEFI_VARS OVMF variable-store template (auto-detected if unset)
WIN11_VM_DISPLAY QEMU display with --window (default: cocoa on macOS, WIN11_VM_DISPLAY QEMU window display (default: cocoa on macOS,
gtk elsewhere) gtk elsewhere)
BASE_BUILD_TMPDIR Parent of the scratch state directory, which needs about BASE_BUILD_TMPDIR Parent of the scratch state directory, which needs about
25 GB (default: /var/tmp) 25 GB (default: /var/tmp)
@@ -77,7 +78,7 @@ force=0
keep_work=0 keep_work=0
compress=0 compress=0
from_img=0 from_img=0
window=0 window=1
output="" output=""
while (($#)); do while (($#)); do
case "$1" in case "$1" in
@@ -86,7 +87,7 @@ while (($#)); do
--keep-work) keep_work=1 ;; --keep-work) keep_work=1 ;;
--compress) compress=1 ;; --compress) compress=1 ;;
--from-img) from_img=1 ;; --from-img) from_img=1 ;;
--window) window=1 ;; --headless) window=0 ;;
-*) usage >&2; exit 2 ;; -*) usage >&2; exit 2 ;;
*) [[ -z "$output" ]] || { usage >&2; exit 2; }; output="$1" ;; *) [[ -z "$output" ]] || { usage >&2; exit 2; }; output="$1" ;;
esac esac
@@ -240,13 +241,15 @@ config_text="$(<"$config_template")"
for placeholder in uefi_code uefi_vars disk_image monitor_socket; do for placeholder in uefi_code uefi_vars disk_image monitor_socket; do
value="${!placeholder}" value="${!placeholder}"
[[ "$value" != *\"* ]] || die "${placeholder} must not contain double quotes: ${value}" [[ "$value" != *\"* ]] || die "${placeholder} must not contain double quotes: ${value}"
config_text="${config_text//"\${${placeholder}}"/"$value"}" # No outer double quotes: bash 3.2 (macOS) keeps inner quotes literally.
config_text=${config_text//"\${${placeholder}}"/"$value"}
done done
printf '%s\n' "$config_text" >"$qemu_config" printf '%s\n' "$config_text" >"$qemu_config"
if ((window)) && [[ -z "${DISPLAY:-}${WAYLAND_DISPLAY:-}" && "$host_os" != darwin ]]; then
window=0
fi
if ((window)); then if ((window)); then
[[ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" || "$host_os" == darwin ]] \
|| die "--window needs a desktop session (DISPLAY or WAYLAND_DISPLAY is not set)."
if [[ -n "${WIN11_VM_DISPLAY:-}" ]]; then display="$WIN11_VM_DISPLAY" if [[ -n "${WIN11_VM_DISPLAY:-}" ]]; then display="$WIN11_VM_DISPLAY"
elif [[ "$host_os" == darwin ]]; then display=cocoa elif [[ "$host_os" == darwin ]]; then display=cocoa
else display=gtk; fi else display=gtk; fi
@@ -263,8 +266,11 @@ qemu_args=(
-netdev user,id=net0 -netdev user,id=net0
-nic none -nic none
-display "$display" -display "$display"
-serial "file:${work}/console.log" -daemonize -pidfile "$pid_file" -serial "file:${work}/console.log" -pidfile "$pid_file"
) )
# The Cocoa/GTK window needs the original process (macOS crashes in fork()), so
# only the VNC display is daemonized; the window runs as a background job.
((window)) || qemu_args+=(-daemonize)
if [[ "$host_os" == darwin && "$arch" == arm64 ]] && ((!from_img)); then if [[ "$host_os" == darwin && "$arch" == arm64 ]] && ((!from_img)); then
# With the HVF in-kernel GICv3, QEMU's usb-storage races on the status packet # 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), # and Windows Setup stalls 10 s after every burst of reads (utmapp/UTM#7970),
@@ -308,7 +314,16 @@ press_key() {
fi fi
} }
if ((window)); then
"$qemu_binary" "${qemu_args[@]}" </dev/null &
qemu_job=$!
until [[ -s "$pid_file" ]]; do
kill -0 "$qemu_job" 2>/dev/null || die "QEMU exited before it started the guest."
sleep 0.2
done
else
"$qemu_binary" "${qemu_args[@]}" "$qemu_binary" "${qemu_args[@]}"
fi
if ((!from_img)); then if ((!from_img)); then
( (
deadline=$((SECONDS + 30)) deadline=$((SECONDS + 30))