#!/bin/sh
# SPDX-FileCopyrightText: 2022-2025 Helmut Grohne <helmut@subdivi.de>
# SPDX-FileCopyrightText: 2023 Johannes Schauer Marin Rodrigues <josch@debian.org>
# SPDX-License-Identifier: MIT

: <<'POD2MAN'
=head1 NAME

debvm-run - Run a VM image created by debvm-create

=head1 SYNOPSIS

B<debvm-run> [B<-g>] [B<-i> F<image>] [B<-s> I<sshport>] [B<--> I<qemu options>]

=head1 DESCRIPTION

B<debvm-run> is essentially a thin wrapper around B<qemu> for running a virtual machine image created by B<debvm-create> or something compatible.
The virtual machine image is expected to be a raw ext4 image.
The architecture of the machine is detected from the contained F</bin/true>.
It must contain a symbolic link pointing to a kernel image at one of F<(|/boot)/vmlinu[xz]> a symbolic link pointing to an initrd image at F<initrd.img> in the same directory as the kernel image.
Both are extracted and passed to B<qemu>.
A net interface configured for user mode is added automatically.

=head1 OPTIONS

=over 8

=item B<--append>=I<cmdline>

While the kernel command line can be modified by passing B<-append> to B<qemu> directly, doing that always replaces the entire command line and thus removes important values passed by B<debvm-run>.
This variant instead appends given command line arguments to the automatic ones.
Repeated use also causes appending rather than replacement.

=item B<-g>, B<--graphical>

By default, the option B<-nographic> is passed to B<qemu> and one interacts with the serial console of the machine.
This configuration is skipped in the presence of this option.
Note that B<debvm-create> defaults to installing a cloud kernel if available, so you may have to pass C<--include=linux-image-generic> during image construction to get graphics drivers.

=item B<-i> F<image>, B<--image>=F<image>

This option specifies the location of the virtual machine image file.
By default F<rootfs.ext4> in the working directory is used.

=item B<--netopt>=I<option>

B<debvm-run> sets up a user mode network by default.
It therefore passes a B<-netdev> option to B<qemu>.
Using this option, you can customize the value of that B<-netdev> option.
For instance, you can set up additional port forwards by passing e.g. C<--netopt hostfwd=:127.0.0.1:8080-:80>.
It can be used multiple times.

=item B<--skip>=I<task>

Skip a particular task or feature.
The option may be specified multiple times or list multiple tasks to be skipped by separating them with a comma.
By default, no tasks are skipped.
The following tasks may be skipped.

=over 4

=item B<balloon>

Do not configure a memory balloon device.
By default, a balloon with free page reporting will be configured such that unused guest memory is actually being released.

=item B<network>

Do not configure a network card.
Use this if you want to configure network on your own.
This should also be passed in addition to passing C<-nic none> when you want to disable networking.

=item B<rngdev>

Do not pass a random number generator device.

=item B<root>

Skip all of the following tasks matching C<root/*>.
If either of these is present, the VM will not boot unless a suitable replacement is added in another way.

=item B<root/cmd>

Since B<debvm-run> uses B<qemu> as bootloader it normally passes the uuid of the root block device via the kernel command line.
This passing can be inhibited to supply a different location.

=item B<root/dev>

A block device for the root filesystem is no longer passed.
This can be used to customize the block device.

=back

=item B<--transport>=I<transport>

When B<debvm> adds devices to B<qemu>, it has to select a transport and it most often guesses B<pci>.
When specifying a different machine such as B<-machine microvm>, a different transport such as B<device> may be needed.

=item B<-s> I<sshport>, B<--sshport>=I<sshport>

If given, B<qemu> is configured to pass connections to I<127.0.0.1:sshport> to port 22 of the virtual machine.
You can connect to your virtual machine without updating your known hosts like this:

    ssh -o NoHostAuthenticationForLocalhost=yes -p $sshport root@127.0.0.1

The option is a shorthand for C<--netopt hostfwd=tcp:127.0.0.1:sshport-:22>.

=item B<--> I<qemu options>

All options beyond a double dash are passed to B<qemu>.
This can be used to configure additional hardware components.
One possible use of this method is passing B<-snapshot> to avoid modifying the virtual machine image.

=back

=head1 EXAMPLES

Run a virtual machine stored in the image F<rootfs.ext4> (the default) with
local port 8022 routed to port 22 of the virtual machine. The B<-snapshot>
argument is passed to QEMU and prevents any permanent changes to
F<rootfs.ext4>, resulting in an ephemeral run.

    debvm-run -s 8022 -i rootfs.ext4 -- -snapshot

Export a filesystem using B<virtiofsd>.

    /usr/libexec/virtiofsd \
        --socket-path ~/.virtiofsd.sock \
        --shared-dir "$SHARED_DIRECTORY" \
        --tag "$SHARE_TAG" \
        --uid-map ":1000:$(id -u):1:" \
        --gid-map ":1000:$(id -g):1:"

Then configure the share in for B<qemu>.

    debvm-run  -- \
        -m 4G \
        -object memory-backend-file,id=mem,size=4G,mem-path=/dev/shm,share=on \
        -numa node,memdev=mem \
        -device "vhost-user-fs-pci,queue-size=1024,chardev=virtiofs,tag=$SHARE_TAG" \
        -chardev socket,id=virtiofs,path=~/.virtiofsd.sock \

The memory passed to B<-m> must equal the size of the backend.
Unlike with B<9pfs>, the Debian B<cloud> kernel variant will support B<virtiofs>.
You can mount the filesystem inside the VM.

    mount -t virtiofs "$SHARE_TAG" /mnt

=head1 FAQ

=over 8

=item The debvm-run console renders wrong.

Make sure C<$TERM> is set to a value known inside the VM.
You may need to install B<ncurses-term> for more definitions.
The serial console will miss events of resizing the terminal emulator.
You may run C<setterm --resize> in that case.

=item How can I kill debvm-run?

The wrapped B<qemu> can be terminated by pressing Ctrl-a x.
Refer to the B<qemu> manual page for more escape sequences.

=back

=head1 LIMITATIONS

Due to the way kernel and bootloader are being extracted before running B<qemu>, one cannot upgrade a kernel and then just reboot.
Attempting to do so, will still use the old kernel.
Instead, B<qemu> must be terminated and B<debvm-run> should be launched again to pick up the new kernel.
In order to avoid accidental reboots, one may pass B<-no-reboot> to B<qemu>.

For 32bit arm, highmem is actively disabled, because the default kernel flavour fails to boot.
If you want to pass more ram, please also pass B<-machine virt> and install a B<lpae> kernel.
Alternatively, use an B<arm64> kernel.

=head1 SEE ALSO

    debvm-create(1) qemu(1)

=cut
POD2MAN

set -u

PATH=/sbin:$PATH

IMAGE=rootfs.ext4
GRAPHICAL=
CMDLINE_APPEND=
NETOPTS=
SKIP=,
SSHPORT=
TRANSPORT=

nth_arg() {
	shift "$1"
	printf "%s" "$1"
}

die() {
	echo "$*" 1>&2
	exit 1
}
with_set_ex() {
	echo "+ $*" 1>&2
	with_set_ex_ret=0
	"$@" || with_set_ex_ret=$?
	if test "$with_set_ex_ret" != 0; then
		die "failed with exit code $with_set_ex_ret"
	fi
}
usage() {
	die "usage: $0 [-g] [-i image] [-s sshport] [-- qemu options]"
}
usage_error() {
	echo "error: $*" 1>&2
	usage
}

opt_append() {
	CMDLINE_APPEND="${CMDLINE_APPEND:+$CMDLINE_APPEND }$1"
}
opt_graphical() {
	GRAPHICAL=1
}
opt_image() {
	IMAGE=$1
}
opt_netopt() {
	NETOPTS="$NETOPTS,$1"
}
opt_skip() {
	SKIP="$SKIP$1,"
}
opt_sshport() {
	SSHPORT=$1
}
opt_transport() {
	TRANSPORT=$1
}

while getopts :gi:s:-: OPTCHAR; do
	case "$OPTCHAR" in
		g)	opt_graphical		;;
		i)	opt_image "$OPTARG"	;;
		s)	opt_sshport "$OPTARG"	;;
		-)
			case "$OPTARG" in
				help)
					usage
				;;
				graphical)
					"opt_$OPTARG"
				;;
				append|image|netopt|skip|sshport|transport)
					test "$OPTIND" -gt "$#" && usage_error "missing argument for --$OPTARG"
					"opt_$OPTARG" "$(nth_arg "$OPTIND" "$@")"
					OPTIND=$((OPTIND+1))
				;;
				append=*|image=*|netopt=*|skip=*|sshport=*|transport=*)
					"opt_${OPTARG%%=*}" "${OPTARG#*=}"
				;;
				*)
					usage_error "unrecognized option --$OPTARG"
				;;
			esac
		;;
		:)
			usage_error "missing argument for -$OPTARG"
		;;
		'?')
			usage_error "unrecognized option -$OPTARG"
		;;
		*)
			die "internal error while parsing command options, please report a bug"
		;;
	esac
done
shift "$((OPTIND - 1))"

if test -n "$SSHPORT"; then
	opt_netopt "hostfwd=tcp:127.0.0.1:$SSHPORT-:22"
fi

test -f "$IMAGE" || die "image '$IMAGE' not found"
test -s "$IMAGE" || die "image '$IMAGE' is empty"

if ! printf '\123\357' | cmp --bytes=2 "$IMAGE" - 1080; then
	die "image '$IMAGE' is not in ext4 format"
fi

# Resolve symbolic link $1 in image $IMAGE
ext2image_readlink() {
	ext2image_readlink_result=$(debugfs "$IMAGE" -R "stat \"$1\"" 2>/dev/null | sed -n -e 's/^Fast link dest: "\(.*\)"$/\1/p;T;q') || return 1
	test -z "$ext2image_readlink_result" && return 1
	echo "$ext2image_readlink_result"
}

check_skip() {
	while :; do
		case "$SKIP" in
			*",$1,"*)	return 0 ;;
		esac
		if test "$1" = "${1%/*}"; then
			return 1
		fi
		set -- "${1%/*}"
	done
}

cleanup() {
	set +x
	test -n "$KERNELTMP" && rm -f "$KERNELTMP"
	# Use ${var+x} to account for $var not being set (we use set -u)
	test -n "${INITRDTMP+x}" && rm -f "$INITRDTMP"
	test -n "${EXT2FSTMP+x}" && rm -f "$EXT2FSTMP"
	test -n "${EXECTMP+x}" && rm -f "$EXECTMP"
}

trap cleanup EXIT INT TERM QUIT

KERNELTMP=$(mktemp)
# $HOST_ARCH_CPU and $GUEST_ARCH_CPU store the name of the Debian CPU name
# for the machine running QEMU and the kernel emulated by QEMU, respectively.
HOST_ARCH_CPU=$(dpkg-architecture --force --query DEB_HOST_ARCH_CPU --host-arch "$(dpkg --print-architecture)")
# We get the initial guess for the kernel architecture by looking at
# executables inside the image with elf-arch. This guess is later refined if
# necessary by checking the kernel image itself with the file utility.
GUEST_ARCH_CPU=
GUEST_ARCH_OS=linux
if debugfs "$IMAGE" -R "stat /" 2>/dev/null | grep -q "Translator:"; then
	GUEST_ARCH_OS=hurd
fi

case "$GUEST_ARCH_OS" in
linux)
	INITRDTMP=$(mktemp)
	if command -v elf-arch >/dev/null 2>&1; then
		for BINARY in /bin/true /bin/bash; do
			debugfs "$IMAGE" -R "cat $BINARY" >"$KERNELTMP"
			test -s "$KERNELTMP" || continue
			ELF_DETECTED_ARCH=$(elf-arch "$KERNELTMP")
			case "$ELF_DETECTED_ARCH" in
			arm)
				# All of arm, armel, armhf are detected as arm, which is ok-ish as that's the CPU name for all of them.
				GUEST_ARCH_CPU=arm
				;;
			*)
				# Most other architectures are properly detected as their Debian architecture name.
				GUEST_ARCH_CPU=$(dpkg-architecture --force --query DEB_HOST_ARCH_CPU --host-arch "$ELF_DETECTED_ARCH")
				;;
			esac
			test -n "$GUEST_ARCH_CPU" || continue
			echo "Detected VM architecture as $GUEST_ARCH_CPU" 1>&2
			break
		done
		if test -z "$GUEST_ARCH_CPU"; then
			GUEST_ARCH_CPU=$HOST_ARCH_CPU
			echo "Failed to detect VM architecture, assuming $GUEST_ARCH_CPU" 1>&2
		fi
	else
		GUEST_ARCH_CPU=$HOST_ARCH_CPU
		echo "Assuming VM architecture as $GUEST_ARCH_CPU" 1>&2
	fi

	for KERNELLINK in vmlinuz vmlinux boot/vmlinuz boot/vmlinux; do
		KERNELNAME=$(ext2image_readlink "$KERNELLINK") && break
	done
	if test "${KERNELLINK%/*}" = "$KERNELLINK"; then
		BOOTDIR=
	else
		BOOTDIR="${KERNELLINK%/*}/"
	fi
	test -n "$KERNELNAME" || die "failed to discover kernel image"
	test "${KERNELNAME#/}" = "$KERNELNAME" && KERNELNAME="$BOOTDIR$KERNELNAME"

	INITRDNAME=$(ext2image_readlink "${BOOTDIR}initrd.img") || die "failed to discover initrd image"
	test -n "$INITRDNAME" || die "failed to discover initrd image"
	test "${INITRDNAME#/}" = "$INITRDNAME" && INITRDNAME="$BOOTDIR$INITRDNAME"

	with_set_ex debugfs "$IMAGE" -R "cat $INITRDNAME" >"$INITRDTMP"
	test -s "$INITRDTMP" || die "E: Extracted $INITRDNAME in $IMAGE is empty"
	with_set_ex debugfs "$IMAGE" -R "cat $KERNELNAME" >"$KERNELTMP"
	test -s "$KERNELTMP" || die "E: Extracted $KERNELNAME in $IMAGE is empty"
	;;
hurd)
	EXT2FSTMP=$(mktemp)
	EXECTMP=$(mktemp)
	with_set_ex debugfs "$IMAGE" -R "cat /hurd/ext2fs.static" >"$EXT2FSTMP"
	test -s "$EXT2FSTMP" || die "E: Extracted /hurd/ext2fs.static in $IMAGE is empty" >&2
	with_set_ex debugfs "$IMAGE" -R "cat /hurd/exec.static" >"$EXECTMP"
	test -s "$EXECTMP" || die "E: Extracted /hurd/exec.static in $IMAGE is empty" >&2

	if command -v elf-arch >/dev/null 2>&1; then
		case "$(elf-arch "$EXECTMP")" in
		amd64)
			die "E: QEMU doesn't support 64 bit multiboot"
			;;
		i386)
			GUEST_ARCH_CPU=i386
			KERNELNAME="/boot/gnumach-1.8-486-up.gz"
			;;
		*)
			die "E: unsupported architecture for hurd: $(elf-arch "$EXECTMP")"
			;;
		esac
		echo "Detected VM architecture for Hurd using elf-arch as $GUEST_ARCH_CPU" 1>&2
	else
		# heuristic: if /boot/gnumach-1.8-amd64-up.gz exists, then this is hurd-amd64
		if [ "$(debugfs "$IMAGE" -R "cat /boot/gnumach-1.8-amd64-up.gz" 2>/dev/null | wc -c)" -gt 0 ]; then
			die "E: QEMU doesn't support 64 bit multiboot"
		else
			GUEST_ARCH_CPU=i386
			KERNELNAME="/boot/gnumach-1.8-486-up.gz"
		fi
		echo "Detected VM architecture for Hurd using kernel name as $GUEST_ARCH_CPU" 1>&2
	fi

	with_set_ex debugfs "$IMAGE" -R "cat $KERNELNAME" | gzip -cd >"$KERNELTMP"
	test -s "$KERNELTMP" || die "E: Extracted $KERNELNAME in $IMAGE is empty" >&2
	;;
*)
	die "E: unsupported OS: $GUEST_ARCH_OS"
	;;
esac

# With the knowledge about the guest system architecture inferred from
# well-known executables inside the image, we check whether maybe we are
# booting a 32 bit system with a 64 bit kernel and if yes, refine our view
# accordingly.
ENABLE_KVM=
if test "$GUEST_ARCH_OS" = linux && command -v file >/dev/null 2>&1; then
	case "$GUEST_ARCH_CPU:$(file -b "$KERNELTMP")" in
		"arm:Linux kernel ARM64 boot executable Image"*)
			if ! linux32 true >/dev/null 2>&1; then
				ENABLE_KVM=no
			fi
			GUEST_ARCH_CPU=arm64
			;;
		# The boot stub looks the same on i386 and amd64, so we
		# actually inspect the kernel version here, which happens to
		# include amd64 for Debian kernels.
		"i386:Linux kernel x86 boot executable bzImage, version "*"-amd64 "*) GUEST_ARCH_CPU=amd64 ;;
		"i386:Linux kernel x86 boot executable, bzImage, version "*"-amd64 "*) GUEST_ARCH_CPU=amd64 ;;
		"mipsel:ELF 64-bit LSB executable,"*) GUEST_ARCH_CPU=mips64el ;;
	esac
fi

case "$ENABLE_KVM" in
"")
	ENABLE_KVM=no
	if test "$HOST_ARCH_CPU" = "$GUEST_ARCH_CPU"; then
		ENABLE_KVM=yes
	fi
	;;
no) : ;;
*)
	die "E: logic error -- ENABLE_KVM must be unset or 'no' here"
	;;
esac

IMAGE_UUID=$(blkid -c /dev/null -s UUID -o value "$IMAGE")

KERNEL_CMDLINE=
if ! check_skip root/cmd; then
	case "$GUEST_ARCH_OS" in
	linux) KERNEL_CMDLINE="root=UUID=$IMAGE_UUID rw" ;;
	hurd) KERNEL_CMDLINE="root=device:hd0" ;;
	*)
		die "E: unsupported OS: $GUEST_ARCH_OS"
		;;
	esac
fi

next_free_fd() {
	next_free_fd_number=$1
	while test -h "/proc/self/fd/$next_free_fd_number"; do
		next_free_fd_number=$((next_free_fd_number + 1))
	done
	echo "$next_free_fd_number"
}

KERNELFD=$(next_free_fd 3)
case "$GUEST_ARCH_OS" in
linux)
	INITRDFD=$(next_free_fd "$((KERNELFD + 1))")
	eval exec "$INITRDFD<"'"$INITRDTMP"'
	rm -f "$INITRDTMP"
	INITRDTMP=
	;;
hurd)
	EXT2FSFD=$(next_free_fd "$((KERNELFD + 1))")
	EXECFD=$(next_free_fd "$((EXT2FSFD + 1))")
	eval exec "$EXT2FSFD<"'"$EXT2FSTMP"'
	eval exec "$EXECFD<"'"$EXECTMP"'
	rm -f "$EXT2FSTMP" "$EXECTMP"
	EXT2FSTMP=
	EXECTMP=
	;;
*)
	die "E: unsupported OS: $GUEST_ARCH_OS"
	;;
esac
eval exec "$KERNELFD<"'"$KERNELTMP"'
rm -f "$KERNELTMP"
KERNELTMP=

set -- \
	-no-user-config \
	-name "debvm-run $IMAGE" \
	-m 1G \
	-kernel "/proc/self/fd/$KERNELFD" \
	"$@"

case "$GUEST_ARCH_OS" in
linux) set -- -initrd "/proc/self/fd/$INITRDFD" "$@" ;;
hurd)
	# Gory details about why and how this works can be found here:
	# https://www.gnu.org/software/hurd/hurd/bootstrap.html
	set -- -initrd "/proc/self/fd/$EXT2FSFD --multiboot-command-line=\${kernel-command-line} --host-priv-port=\${host-port} --device-master-port=\${device-port} --exec-server-task=\${exec-task} -T typed \${root} \$(task-create) \$(task-resume),/proc/self/fd/$EXECFD \$(exec-task=task-create)" "$@"
	;;
*)
	die "E: unsupported OS: $GUEST_ARCH_OS"
	;;
esac

# This utilizes the QEMU Debian package symlink mapping that ensures that
# calling qemu-system-${DEB_HOST_ARCH_CPU} will run the QEMU binary providing
# the correct emulator for that CPU in trixie and later.
QEMU="qemu-system-$GUEST_ARCH_CPU"
CPU=
MACHINE=
MAX_SMP=

case "$GUEST_ARCH_CPU" in
	amd64)
		QEMU=qemu-system-x86_64
		MACHINE="type=q35"
	;;
	arm)
		CPU=max
		MACHINE="type=virt,highmem=off,gic-version=2"
		MAX_SMP=8
	;;
	arm64)
		QEMU=qemu-system-aarch64
		CPU=max,pauth-impdef=on
		MACHINE="type=virt,gic-version=max"
	;;
	m68k)
		MACHINE="type=virt"
		MAX_SMP=1
		: "${TRANSPORT:=device}"
	;;
	loong64)
		QEMU=qemu-system-loongarch64
	;;
	mips64el)
		CPU=5KEc
		MAX_SMP=1
	;;
	mipsel)
		MAX_SMP=1
	;;
	powerpc)
		QEMU=qemu-system-ppc
		MAX_SMP=1
	;;
	ppc64el)
		QEMU=qemu-system-ppc64
	;;
	riscv64)
		MACHINE="type=virt"
	;;
	sparc64)
		MAX_SMP=1
		opt_skip rngdev
	;;
esac

# Assign the default late to allow both cli and arch-specific overrides.
: "${TRANSPORT:=pci}"

# If the image filename contains a comma, then that comma must be escaped by
# prefixing it with another comma or otherwise output filenames are able to
# inject drive options to qemu (and load the wrong file).
IMAGE_ESCAPED="$(printf "%s" "$IMAGE" | sed 's/,/,,/g')"

if ! check_skip root/dev; then
	qemu_drive_opts="media=disk,format=raw,discard=unmap,file=$IMAGE_ESCAPED,cache=unsafe"
	if test "$GUEST_ARCH_OS" != hurd; then
		qemu_drive_opts="id=root,if=none,$qemu_drive_opts"
		set -- -device "virtio-blk-$TRANSPORT,drive=root,serial=root" "$@"
	fi
	set -- -drive "$qemu_drive_opts" "$@"
fi

if test "$ENABLE_KVM" = yes; then
	if ! command -v "$QEMU" >/dev/null 2>&1; then
		# Fall back to kvm in case we badly guessed qemu.
		QEMU=kvm
	fi
	MACHINE="${MACHINE:+$MACHINE,}accel=kvm:tcg"
	# While kvm will fall back gracefully, only override CPU when we expect
	# kvm to work.
	if test -w /dev/kvm; then
		CPU=max
		# kvm: "max" will become "host", intended.
		# tcg: "max" will actually work, "host" would not.
	fi
fi

if ! command -v "$QEMU" >/dev/null 2>&1; then
	die "cannot find qemu executable '$QEMU'"
fi

if test -n "$MACHINE"; then
	set -- -machine "$MACHINE" "$@"
fi
if test -n "$CPU"; then
	set -- -cpu "$CPU" "$@"
fi
if test -z "$MAX_SMP" || test "$MAX_SMP" -gt 1; then
	NPROC=$(nproc)
	if test "$NPROC" -gt 1; then
		test -n "$MAX_SMP" && test "$NPROC" -gt "$MAX_SMP" && NPROC=$MAX_SMP
		set -- -smp "$NPROC" "$@"
	fi
fi
if ! check_skip rngdev; then
	set -- \
		-device "virtio-rng-$TRANSPORT,rng=rng0" \
		-object rng-random,filename=/dev/urandom,id=rng0 \
		"$@"
fi
if ! check_skip balloon; then
	set -- \
		-object iothread,id=balloon-iothread \
		-device "virtio-balloon-$TRANSPORT,free-page-hint=on,free-page-reporting=on,iothread=balloon-iothread" \
		"$@"
fi

if test -z "$GRAPHICAL"; then
	set -- -nographic "$@"
	case "$GUEST_ARCH_OS-$GUEST_ARCH_CPU" in
		linux-amd64|linux-i386)
			KERNEL_CMDLINE="${KERNEL_CMDLINE:+"$KERNEL_CMDLINE "}console=ttyS0"
		;;
		hurd-amd64)
			die "E: logic error. QEMU doesn't support 64 bit multiboot"
		;;
		hurd-i386)
			KERNEL_CMDLINE="${KERNEL_CMDLINE:+"$KERNEL_CMDLINE "}console=com0"
		;;
	esac
	if test -t 0 && test -t 1 && test -n "${TERM:-}"; then
		KERNEL_CMDLINE="${KERNEL_CMDLINE:+"$KERNEL_CMDLINE "}TERM=$TERM"
	fi
else
	case "$GUEST_ARCH_CPU" in
		amd64|i386)
			set -- -vga virtio "$@"
		;;
		*)
			set -- \
				-device "virtio-gpu-gl-$TRANSPORT" \
				-display gtk,gl=on \
				"$@"
		;;
	esac
	set -- \
		-device "virtio-keyboard-$TRANSPORT" \
		-device "virtio-tablet-$TRANSPORT" \
		"$@"
fi

DNSSEARCH=$(dnsdomainname)
if test -z "$DNSSEARCH"; then
	DNSSEARCH=$(sed -n 's/^\s*search\s*\.\?//p;T;q' /etc/resolv.conf)
fi
if test -n "$DNSSEARCH"; then
	NETOPTS=",domainname=$DNSSEARCH$NETOPTS"
fi

if test -n "$CMDLINE_APPEND"; then
	KERNEL_CMDLINE="${KERNEL_CMDLINE:+"$KERNEL_CMDLINE "}$CMDLINE_APPEND"
fi
if test -n "$KERNEL_CMDLINE"; then
	set -- -append "$KERNEL_CMDLINE" "$@"
fi

if ! check_skip network; then
	set -- -netdev "user,id=net0$NETOPTS" "$@"
	if test "$GUEST_ARCH_OS" = hurd; then
		# https://www.gnu.org/software/hurd/hurd/running/qemu.html
		# The e1000 is preferred over rtl8139 because the latter has a deadlock
		# issue that happens from time to time.
		set -- -device e1000,netdev=net0 "$@"
	else
		set -- -device "virtio-net-$TRANSPORT,netdev=net0" "$@"
	fi
fi

echo "+ $QEMU $*" 1>&2
exec "$QEMU" "$@"
