#!/bin/sh -e
# run-hurd.sh, a script to fetch and run Debian GNU (Hurd) using qemu, aspiring to be fool-proof
# Copyright (C) 2026 alicia@ion.nu
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
bit64=false
redownload=false
fsck=false
daemon=false
expand=''
sshport='127.0.0.1:2222'
video=''
ram='2G'
while getopts "6rfde:s:v:m:" opt; do
case "$opt" in
6) bit64=true;;
r) redownload=true;;
f) fsck=true;;
d) daemon=true;;
v) video="-display ${OPTARG}";;
e) expand="$OPTARG"
if ! echo "$expand" | grep -q '[0-9]\+[BKMGTP]'; then
echo 'Invalid format for expansion. Expected number followed by unit (B for bytes, K for kilobytes, M for megabytes etc.)'
exit 1
fi;;
m) ram="$OPTARG" # TODO: Warn about >16GB issue?
if ! echo "$ram" | grep -q '[0-9]\+[BKMGTP]'; then
echo 'Invalid format for RAM. Expected number followed by unit (B for bytes, K for kilobytes, M for megabytes etc.)'
exit 1
fi;;
s) sshport="$OPTARG"
if ! echo "$sshport" | grep -q '^[0-9.]*:[0-9]\+$'; then
echo 'Invalid format for SSH host/port. Expected format "host:port" where host is optional. No host = remote accessible'
exit 1
fi;;
*) echo "Usage: ${0} [options]"
echo 'Options include:'
echo ' -6 = use the new 64bit build'
echo ' -r = redownload instead of booting existing image'
echo ' -f = run a filesystem check (requires losetup and fsck.ext2)'
echo ' -d = daemonize, start qemu and return to the shell'
echo ' -e = expand disk image to the given size'
echo ' -s [host]: = set port for SSH forward (default: 127.0.0.1:2222)'
echo ' -v = set the Qemu display option. e.g. curses or vnc=:0'
echo ' curses is good for non-GUI environments and to translate'
echo ' keypresses from host keyboard layout.'
exit;;
esac
done
# Some sanity checks before we begin
depcheck()
{
local cmd="$1"
local guixpkg="$2"
local pacmanpkg="$3"
local aptpkg="$4"
local dnfpkg="$5"
# Cascade package name for missing parameters
if [ -z "$guixpkg" ]; then guixpkg="$cmd"; fi
if [ -z "$pacmanpkg" ]; then pacmanpkg="$guixpkg"; fi
if [ -z "$aptpkg" ]; then aptpkg="$pacmanpkg"; fi
if [ -z "$dnfpkg" ]; then dnfpkg="$aptpkg"; fi
if [ ! -f "`which "$cmd"`" ]; then # Using -f because some implementations of 'which' make simply return code or presence of output unreliable indicators
echo "${cmd} not found. You can likely install it with one of the commands below:"
echo "guix install ${guixpkg}"
echo "sudo pacman -S ${pacmanpkg}"
echo "sudo apt install ${aptpkg}"
echo "sudo dnf install ${dnfpkg}"
exit 1
fi
}
if [ ! -f "`which 'sudo'`" ]; then # No sudo = make our own using su
sudo()
{
local cmd="$@"
echo "su -c '${cmd}'" >&2
PATH="/sbin:/usr/sbin:${PATH}" su -c "$cmd"
}
fi
depcheck qemu-system-x86_64 qemu qemu-system-x86
if [ ! -e debian-hurd.img ] || "$redownload"; then
depcheck wget
rm -f debian-hurd.img.tar.gz
if "$bit64"; then
wget 'https://cdimage.debian.org/cdimage/ports/latest/hurd-amd64/debian-hurd.img.tar.gz'
else
wget 'https://cdimage.debian.org/cdimage/ports/latest/hurd-i386/debian-hurd.img.tar.gz'
fi
image="`tar -xvzf debian-hurd.img.tar.gz`"
mv "$image" debian-hurd.img
fi
if "$fsck"; then
# TODO: Dynamically figure out which way (if any, maybe user is root) to run these as root
# We set PATH here because some distros hide tools like fsck.ext2 in paths that are only in root's PATH by default, so without it we get an error here even though sudo finds it
PATH="/sbin:/usr/sbin:${PATH}" depcheck fsck.ext2 e2fsprogs
disk="`sudo losetup -fP --show debian-hurd.img`"
if [ -z "$disk" ]; then echo 'Failed to losetup the disk image. Is losetup installed? (part of util-linux)'; exit 1; fi
# TODO: Bundle these sudos together? (cuts down on password prompts, especially when falling back to su)
if [ -e "${disk}p5" ]; then
sudo fsck.ext2 -fy "${disk}p5" || true
else
sudo fsck.ext2 -fy "${disk}p2" || true # 32bit doesn't use the extended partition
fi
sudo losetup -d "$disk"
fi
if [ -n "$expand" ]; then
PATH="/sbin:/usr/sbin:${PATH}" depcheck parted
PATH="/sbin:/usr/sbin:${PATH}" depcheck fsck.ext2 e2fsprogs
num="`echo "$expand" | sed -e 's/[A-Z]$//'`"
unit="`echo "$expand" | sed -e 's/^[0-9]*//'`"
dd if=/dev/zero of=debian-hurd.img bs="1${unit}" count=0 seek="$num"
disk="`sudo losetup -fP --show debian-hurd.img`"
# TODO: Unify the losetup calls?
if [ -z "$disk" ]; then echo 'Failed to losetup the disk image. Is losetup installed?'; exit 1; fi
# TODO: Bundle these sudos together? (cuts down on password prompts, especially when falling back to su)
sudo parted "$disk" resizepart 2 '100%' # Resize the extended partition
if [ -e "${disk}p5" ]; then
sudo parted "$disk" resizepart 5 '100%' # Resize the filesystem's partition therein
sudo fsck.ext2 -fy "${disk}p5" || true
sudo resize2fs "${disk}p5"
else
sudo fsck.ext2 -fy "${disk}p2" || true
sudo resize2fs "${disk}p2" # No extended partition for 32bit
fi
sudo losetup -d "$disk"
fi
runqemu()
{
kvm="`grep -q ' \(svm\|vmx\)' /proc/cpuinfo && echo '--enable-kvm' || true`"
# I think 32bit code should work even if we run x86_64 qemu?
# Something is broken on 64bit Hurd with 4+GB RAM but -M q35 evades this. But 32bit breaks with it so it needs to be conditional. >16GB remains broken
if "$bit64"; then machine='-M q35'; else machine=''; fi
qemu-system-x86_64 ${video} ${machine} ${kvm} -m "$ram" -drive cache=writeback,file=debian-hurd.img -net user,hostfwd="tcp:${sshport}-:22" -net nic,model=e1000
}
if "$daemon"; then
runqemu > /dev/null 2> /dev/null &
else
runqemu
fi