xref: /freebsd/tools/boot/boot-test.sh (revision 041802b362aab8786419618ead6e50c389eb875a)
1#!/bin/sh
2#
3# boot-test.sh - Automated boot loader regression tests
4#
5# Builds a minimal bootable tree, assembles disk images for all supported boot
6# configurations, then runs each in QEMU with a timeout looking for "SUCCESS".
7#
8# All tests run as an unprivileged user. No root required, with one exception:
9# netboot-bios/netboot-efi need a real tap(4) device + dnsmasq (so DHCP can
10# carry a root-path) instead of QEMU's slirp networking, and both creating a tap
11# and giving it an address require root. That setup runs once (via sudo,
12# prompting interactively) and is left running -- later runs detect the existing
13# setup and skip sudo entirely.  Assumes buildworld and buildkernel have already
14# been done for the target architecture.
15#
16# Usage:
17#   cd /usr/src/stand && sh ../tools/boot/boot-test.sh [options]
18#
19# Options:
20#   -a ARCH	Architecture to test; repeat -a to test several
21#		(default: host arch)
22#		Supported: amd64, aarch64, armv7, riscv64, powerpc, powerpc64,
23#		powerpc64le, loongarch64
24#		(loongarch64 is disabled/excluded from -A until the LoongArch
25#		port lands in-tree; usable directly via -a once it has)
26#   -A		Test every supported architecture
27#   -b		Skip build/install phase (reuse existing tree)
28#   -B		Skip build/install and image creation (reuse existing images)
29#   -l          Tell us the log directory and exit
30#   -j JOBS	Max parallel QEMU instances (default: unlimited)
31#   -o DIR	Output directory for images and logs
32#   -t REGEX	Only run tests matching REGEX
33#   -T SECONDS	QEMU timeout (default: per-arch, 60; 180 for powerpc64)
34#   --netboot-teardown
35#		Destroy the tap(4)/dnsmasq netboot setup (run with sudo)
36
37set -e
38
39die() {
40    echo "FATAL: $*" >&2
41    exit 1
42}
43
44# FreeBSD port package (origin) that provides a command or file; "" = base
45# system.
46pkg_for() {
47    case "$1" in
48    jq)			echo textproc/jq ;;
49    expect)		echo lang/expect ;;
50    qemu-system-*)	echo emulators/qemu ;;
51    *ipxe*)		echo sysutils/ipxe ;;
52    *syslinux*|*memdisk*) echo sysutils/syslinux ;;
53    *edk2*)		echo emulators/qemu ;;	# edk2-*.fd ship with qemu
54    python3)		echo lang/python3 ;;
55    *)			echo "" ;;		# makefs/mkimg etc. = base
56    esac
57}
58
59# Hard requirement: a command that must be in PATH, else abort naming the pkg.
60need_cmd() {
61    which "$1" >/dev/null 2>&1 && return 0
62    p=$(pkg_for "$1")
63    [ -n "${p}" ] && die "$1 not found; install with: pkg install ${p##*/}"
64    die "$1 not found (expected in the base system)"
65}
66
67# Optional requirement: a file for a feature; warn + skip (return 1) if absent.
68have_file() {
69    [ -e "$1" ] && return 0
70    p=$(pkg_for "$1")
71    echo "  WARNING: $1 missing${p:+; pkg install ${p##*/}} -- skipping" >&2
72    return 1
73}
74
75# --------------------------------------------------------------------------
76# Architecture configuration
77#
78# Per-arch parameters live in boot-test.json, as an arch object merged over a
79# "defaults" object. Arrays are expanded to a space separated list. Missing
80# values default to an empty string. ARCH_CAPS is a cached copy of the
81# capabilities array for the architecture. TARGET and TARGET_ARCH are cached due
82# to heavy use. Other parameters are fetched as needed as their use is
83# infrequent.
84# --------------------------------------------------------------------------
85
86# Test whether the current arch declares a capability, e.g. "has efi".
87# "no_release" is special: it means the arch has never had a FreeBSD
88# release, so there's no ISO to pull a minimal userland from -- see
89# install_minimal_userland_world().
90has() {
91    case " ${ARCH_CAPS} " in
92	*" $1 "*) return 0 ;;
93    esac
94    return 1
95}
96
97# Fetch the named parameter for this $ARCH.
98param() {
99    jq -r --arg a "${ARCH}" --arg k "$1" '
100	(.defaults + .arch[$a])[$k] as $v
101	| if   ($v | type) == "array" then $v | join(" ")
102	  elif $v == null             then ""
103	  else                             $v end
104    ' "${CONF}"
105}
106
107# Validate ${ARCH} and cache the parameters that are heavily used in global
108# variables.
109load_arch_config() {
110    jq -e --arg a "${ARCH}" '.arch | has($a)' "${CONF}" >/dev/null 2>&1 \
111	|| die "Unknown architecture: ${ARCH}"
112    ARCH_CAPS=$(param caps)
113    TARGET=$(param target)
114    TARGET_ARCH=$(param target_arch)
115}
116
117# Derive every per-arch value for ${1} into the ARCH_*/TARGET/OUTDIR/... globals.
118# Callers isolate architectures via subshells (build) or job backgrounding (run)
119# -- both snapshot these globals -- so they never collide across arches, and we
120# never have to pass the whole bundle around.
121setup_arch_env() {
122    ARCH=$1
123    load_arch_config
124    TIMEOUT=${TIMEOUT_OVERRIDE:-$(param timeout)}
125    MK="make TARGET=${TARGET} TARGET_ARCH=${TARGET_ARCH}"
126    ARCH_OBJDIR=$(${MK} -v .OBJDIR)
127    [ -n "${ARCH_OBJDIR}" ] || die "Cannot determine OBJDIR for ${ARCH}"
128    OUTDIR=${OUTDIR_OVERRIDE:-${ARCH_OBJDIR}/boot-test}
129    IMGDIR=${OUTDIR}/images
130    LOGDIR=${OUTDIR}/logs
131    DESTDIR=${OUTDIR}/tree
132    TESTLIST=${OUTDIR}/test-list.txt
133    mkdir -p ${IMGDIR} ${LOGDIR}
134}
135
136# --- Configuration ---
137
138SKIP_BUILD=false
139SKIP_IMAGES=false
140TEST_FILTER=""
141MAX_JOBS=0
142OUTDIR_OVERRIDE=""	# from -o; only valid with a single arch
143TIMEOUT_OVERRIDE=""	# from -T; else each arch's param timeout
144ARCHES=""		# from -a (repeatable) / -A; defaults to $(uname -p)
145ALL=false
146
147# State shared by tap(4)/dnsmasq netboot networking (see netboot_network_setup /
148# netboot_helper below). Key off user's ID to allow multiple people to
149# run the script at the same time.
150NETBOOT_STATE_DIR=${TMPDIR:-/tmp}/boot-test-net.${SUDO_UID:-$(id -u)}
151
152# --netboot-helper/--netboot-teardown are internal entry points used to run
153# privileged setup/teardown via sudo (see netboot_network_setup); they bypass
154# normal option parsing and are dispatched once every function is defined, in
155# the Main section at the bottom of this script.
156NETBOOT_MODE=""
157case "$1" in
158    --netboot-helper)   NETBOOT_MODE=helper; NETBOOT_HELPER_PLAN=$2 ;;
159    --netboot-teardown) NETBOOT_MODE=teardown ;;
160esac
161
162do_report_dirs=false
163if [ -z "${NETBOOT_MODE}" ]; then
164    while getopts "a:AbBlj:o:t:T:" opt; do
165	case "$opt" in
166	    a) ARCHES="${ARCHES} $OPTARG" ;;
167	    A) ALL=true ;;
168	    b) SKIP_BUILD=true ;;
169	    B) SKIP_BUILD=true; SKIP_IMAGES=true ;;
170	    j) MAX_JOBS="$OPTARG" ;;
171	    l) do_report_dirs=true ;;
172	    o) OUTDIR_OVERRIDE="$OPTARG" ;;
173	    t) TEST_FILTER="$OPTARG" ;;
174	    T) TIMEOUT_OVERRIDE="$OPTARG" ;;
175	    ?) echo "Usage: $0 [-a arch]... | -A] [-b] [-B] [-t regex] [-T secs] [-j jobs] [-o dir]" >&2
176	       exit 1 ;;
177	esac
178    done
179
180    # Resolve the config file next to this script before we cd elsewhere.
181    CONF="$(cd "$(dirname "$0")" && pwd)/boot-test.json"
182    [ -f "${CONF}" ] || die "Config file not found: ${CONF}"
183
184    SRCTOP=$(make -v SRCTOP) || die "Run from stand/ directory in a FreeBSD source tree"
185    cd ${SRCTOP}/stand
186
187    # Build the architecture list from json. Partially supported architectures
188    # are omitted from -A, but accessible with a direct -a.
189    if ${ALL}; then
190	for a in $(jq -r '.arch | keys_unsorted[]' "${CONF}"); do
191	    if [ $(jq ".arch.${a}.disabled" "${CONF}") != "true" ]; then
192		ARCHES="$ARCHES $a"
193	    fi
194	done
195    fi
196    [ -n "${ARCHES}" ] || ARCHES=$(uname -p)
197
198    # -o names one output directory, so it only makes sense for a single arch.
199    if [ -n "${OUTDIR_OVERRIDE}" ] && [ $(echo ${ARCHES} | wc -w) -gt 1 ]; then
200	die "-o cannot be combined with multiple architectures"
201    fi
202fi
203
204# The smallest FAT32 filesystem is 33292 KB
205espsize=33292
206
207# GPT attribute bit that marks the partition to boot, from
208# sys/sys/disk/gpt.h (GPT_ENT_ATTR_BOOTME).  Used for the A/B test.
209GPT_ATTR_BOOTME=59
210
211# Linux kernel version for linuxboot tests
212LINUX_VERSION=6.18.2
213
214# --------------------------------------------------------------------------
215# QEMU command builders
216#
217# qemu_base wraps the constant bits -- binary, memory, machine, per-arch extra
218# flags, and the -nographic/serial tail -- around the device arguments each
219# specific builder passes in (disks, CDs, firmware, bios).
220# --------------------------------------------------------------------------
221
222qemu_base() {
223    echo "$(param qemu_bin) -m 1g $(param qemu_machine) $(param qemu_extra) $* -nographic -monitor none -serial stdio"
224}
225
226# -drive for the EFI firmware pflash; empty when the arch has no separate
227# firmware (e.g. riscv64's u-boot payload).
228qemu_efi_firmware() {
229    [ -z "$(param efi_firmware)" ] && return 0
230    echo "-drive file=$(param efi_firmware),format=raw,if=pflash,readonly=on"
231}
232
233# Custom OpenBIOS cached beside the ISOs, if present (QEMU's bundled one is
234# missing fixes we need for now); empty otherwise.
235qemu_ofw_bios() {
236    [ -f "${ISODIR}/openbios-ppc" ] || return 0
237    echo "-bios ${ISODIR}/openbios-ppc"
238}
239
240qemu_bios()       { qemu_base "-drive file=$1,format=raw"; }
241qemu_efi()        { qemu_base "$(qemu_efi_firmware) -drive file=$1,format=raw"; }
242qemu_bios_cdrom() { qemu_base "-cdrom $1"; }
243qemu_efi_cdrom()  { qemu_base "$(qemu_efi_firmware) -cdrom $1"; }
244
245# OFW disk on the default (macio IDE) bus: OpenBIOS aliases it "hd" and
246# auto-probes hd:,\\:tbxi for the Apple_Bootstrap (boot1.hfs) partition.
247# virtio disks are not reachable as "hd", so OF finds nothing and drops to "0 >".
248qemu_ofw()        { qemu_base "$(qemu_ofw_bios) -drive file=$1,format=raw"; }
249# -boot d boots the (macio IDE) CD-ROM, which OpenBIOS probes cd:,\\:tbxi.
250qemu_ofw_cdrom()  { qemu_base "$(qemu_ofw_bios) -boot d -cdrom $1"; }
251
252# pseries PReP disk boot: virtio-blk (vtbd0), matching freebsd-ci; SLOF finds
253# the PReP boot partition on it and runs boot1.elf.
254qemu_prep()       { qemu_base "-drive if=none,file=$1,format=raw,id=hd0 -device virtio-blk,drive=hd0"; }
255# pseries CD boot: SLOF boots the El Torito CHRP image; -boot d selects the CD.
256qemu_prep_cdrom() { qemu_base "-cdrom $1 -boot d"; }
257
258# Direct linuxboot: hand the Linux kernel and initrd to QEMU on the command
259# line (-kernel/-initrd) rather than off an ESP.  Used by platforms with no
260# EFI/ESP (e.g. powerpc64le/pseries); the FreeBSD root disk is attached the
261# same way as every other test.
262qemu_linuxboot() {
263    extra="-kernel $2 -initrd $3"
264    [ -n "$(param linux_console)" ] && extra="${extra} -append $(param linux_console)"
265    qemu_base "${extra} -drive file=$1,format=raw"
266}
267
268# Netboot over $1 -- vmnet(4): dnsmasq owns DHCP/TFTP/root-path, which we use so
269# we use tftp, not NFS, for all the files. QEMU's netdev type is still "tap" (it
270# treats vmnet(4) and tap(4) identically).  $2 = bios|efi (efi adds the pflash
271# firmware; bios relies on the NIC's PXE option ROM).
272qemu_netboot() {
273    netif=$1
274    fw=""
275    [ "$2" = efi ] && fw="$(qemu_efi_firmware)"
276    qemu_base "${fw} -netdev tap,id=net0,ifname=${netif},script=no,downscript=no -device $(param netboot_nic),netdev=net0 -boot n"
277}
278
279# RAM-disk netboot (x86 EFI).  Boots iPXE from -hda (edk2's own PXE is disabled
280# via fw_cfg); iPXE DHCPs, fetches the bootfile (an iPXE script) over TFTP, and
281# chains loader.efi with memdisk=<url>.  The loader downloads that image and
282# boots it entirely from RAM -- no NFS and no DHCP root-path needed.  Mirrors
283# ~/memdisk/do-memddisk-efi.  ${OUTDIR}/netboot-vars.fd is a writable edk2 vars
284# copy made by assemble_netboot.
285#
286# We'll need to to http boots in the future, and that will likely require
287# we don't use the ipxe USB path we use here. We use that because Tianocore
288# expects http/https booting when the obvious '-boot n' sort of things
289# are used.
290#
291# -device virtio-rng-pci: EDK II's network stack has required an RNG
292# device since CVE-2023-45237.
293qemu_netboot_ramdisk() {
294    netif=$1
295    echo "$(param qemu_bin) -M q35 -cpu max -m 2g \
296	-drive if=pflash,format=raw,readonly=on,file=$(param efi_firmware) \
297	-drive if=pflash,format=raw,file=${OUTDIR}/netboot-vars.fd \
298	-hda ${OUTDIR}/netboot-ipxe.img \
299	-device virtio-net,netdev=net0 \
300	-netdev tap,id=net0,ifname=${netif},script=no,downscript=no \
301	-device virtio-rng-pci \
302	-fw_cfg name=opt/org.tianocore/IPv4PXESupport,string=no \
303	-fw_cfg name=opt/org.tianocore/IPv6PXESupport,string=no \
304	-nographic -monitor none -serial stdio"
305}
306
307# --------------------------------------------------------------------------
308# Phase 0: Extract minimal userland from release ISO
309# --------------------------------------------------------------------------
310
311# Binaries needed for a minimal bootable userland.
312# Libraries are inferred from these via ldd on the host equivalents.
313USERLAND_BINS="sbin/fastboot sbin/halt sbin/init bin/sh sbin/sysctl"
314
315ISODIR=${HOME}/iso
316
317find_iso() {
318    local isotgt=${TARGET}
319    [ ${TARGET} != ${TARGET_ARCH} ] && isotgt="${isotgt}-${TARGET_ARCH}"
320    local isoname="${ISODIR}/FreeBSD-$(param freebsd_version)-RELEASE-${isotgt}-disc1.iso.xz"
321    [ -f "${isoname}" ] && echo "${isoname}" && return
322    die "No ISO found for ${ARCH}: ${isoname} not found"
323}
324
325# Paths (binaries + their libraries + rtld) to pull into DESTDIR for a minimal
326# userland. Library *names* are discovered via ldd on the HOST's own copies of
327# USERLAND_BINS: those paths (e.g. /lib/libc.so.7) are the same across
328# architectures, only the file contents differ, so this works even when the
329# files themselves are about to come from a foreign-arch source.
330userland_extract_list() {
331    local host_bins=""
332    for b in ${USERLAND_BINS}; do
333	host_bins="${host_bins} /${b}"
334    done
335    local lib_paths=$(ldd ${host_bins} 2>/dev/null \
336	| awk 'NF == 4 { print $3 }' | sort -u)
337
338    local list=""
339    for b in ${USERLAND_BINS}; do
340	list="${list} ./${b}"
341    done
342    for l in ${lib_paths}; do
343	list="${list} .${l}"
344    done
345    list="${list} ./libexec/ld-elf.so.1"
346
347    # Some architectures (e.g., armv7) need libgcc_s.so.1 even though the host
348    # binaries don't. Always try to extract it. We'll ignore it if not there.
349    list="${list} ./lib/libgcc_s.so.1"
350    echo "${list}"
351}
352
353# Extract the minimal userland from a release ISO -- the normal path for any
354# arch that has actually shipped a FreeBSD release.
355install_minimal_userland_iso() {
356    # Split the declaration from the assignment: `local iso=$(find_iso)` would
357    # swallow find_iso's exit status (local returns 0), so its die() -- which
358    # runs in the $() subshell -- would not stop this script.
359    local iso
360    iso=$(find_iso) || exit 1
361    echo "  Extracting minimal userland from release ISO ${iso}..."
362    tar -C ${DESTDIR} -xf ${iso} $(userland_extract_list) \
363	>> ${LOGDIR}/installuserland.log 2>&1 || true
364}
365
366# Cross-build the minimal userland via installworld instead of an ISO -- for
367# architectures that have never had a FreeBSD release (no_release cap), so no
368# ISO exists or ever will for the current version. Assumes buildworld has
369# already been done for TARGET/TARGET_ARCH, same precondition build_tree
370# already has for buildkernel.
371install_minimal_userland_world() {
372    echo "  Cross-installing minimal userland via installworld (no release for ${ARCH})..."
373    local worlddir=${OUTDIR}/world
374    rm -rf ${worlddir}
375    mkdir -p ${worlddir}
376    (cd ${SRCTOP} && ${MK} installworld \
377	DESTDIR=${worlddir} \
378	MK_MAN=no \
379	MK_TESTS=no \
380	MK_DEBUG_FILES=no \
381	MK_INSTALL_AS_USER=yes) > ${LOGDIR}/installworld.log 2>&1 \
382	|| die "installworld failed (see ${LOGDIR}/installworld.log)"
383
384    local item src
385    for item in $(userland_extract_list); do
386	src=${worlddir}/${item#.}
387	[ -f "${src}" ] || continue
388	mkdir -p ${DESTDIR}/$(dirname ${item#.})
389	cp -p ${src} ${DESTDIR}/${item#.}
390    done
391    rm -rf ${worlddir}
392}
393
394install_minimal_userland() {
395    if has no_release; then
396	install_minimal_userland_world
397    else
398	install_minimal_userland_iso
399    fi
400}
401
402# --------------------------------------------------------------------------
403# Phase 1: Build the boot tree
404# --------------------------------------------------------------------------
405
406build_tree() {
407    echo "=== Phase 1: Building boot tree ==="
408
409    rm -rf ${DESTDIR}
410    mkdir -p ${DESTDIR}/boot/defaults
411    mkdir -p ${DESTDIR}/boot/kernel
412    mkdir -p ${DESTDIR}/boot/uboot
413    mkdir -p ${DESTDIR}/sbin ${DESTDIR}/bin \
414	${DESTDIR}/lib ${DESTDIR}/libexec \
415	${DESTDIR}/etc ${DESTDIR}/dev
416
417    # Install kernel
418    # I'd prefer this to be MINIMAL, but GENERIC is needed until I work out what
419    # different devices we boot from...
420    (cd ${SRCTOP} && ${MK} installkernel \
421	KERNCONF=$(param kernconf) \
422	MODULES_OVERRIDE="ufs zfs acl_nfs4 crypto zlib cd9660" \
423	DESTDIR=${DESTDIR} \
424	MK_KERNEL_SYMBOLS=no \
425	MK_INSTALL_AS_USER=yes) > ${LOGDIR}/installkernel.log 2>&1 \
426	|| die "Kernel install failed (see ${LOGDIR}/installkernel.log)"
427
428    # Install boot loaders
429    ${MK} buildenv \
430	DESTDIR=${DESTDIR} \
431	MK_MAN=no \
432	MK_INSTALL_AS_USER=yes \
433	MK_DEBUG_FILES=no \
434	BUILDENV_SHELL="make all install" \
435	>> ${LOGDIR}/installloader.log 2>&1 \
436	|| die "Boot loader install failed (see ${LOGDIR}/installloader.log)"
437
438    # Install minimal userland (works for both native and cross builds)
439    install_minimal_userland
440
441    # Remove default loader symlinks -- we add them back per-image via
442    # mtree overlays to test each loader variant individually.
443    # /boot/loader is the BIOS stage-3 (amd64 only).
444    # /boot/loader.efi is what boot1.efi chainloads (all EFI platforms).
445    # OFW/PReP are the exception: their /boot/loader is the one real loader
446    # (no lua/4th/simp variants are built), chainloaded by boot1.hfs (mac99)
447    # or boot1.elf (pseries PReP), so keep it in the tree.
448    has ofw || has prep || rm -f ${DESTDIR}/boot/loader
449    rm -f ${DESTDIR}/boot/loader.efi
450
451    # Serial console configuration.  boot.config is consumed only by the
452    # BIOS boot blocks; its -h/-D/-S flags are meaningless on EFI/OFW, so
453    # only write it where BIOS booting is supported.
454    if has bios; then
455	echo -h -D -S115200 > ${DESTDIR}/boot.config
456    fi
457    # Unified loader.conf: always load ufs, zfs, and cd9660
458    cat > ${DESTDIR}/boot/loader.conf <<EOF
459boot_serial=YES
460comconsole_speed=115200
461autoboot_delay=1
462ufs_load="YES"
463zfs_load="YES"
464cd9660_load="YES"
465EOF
466    local hints=$(param hints)
467    if [ -n "${hints}" ] && [ -f "${SRCTOP}/${hints}" ]; then
468	cp "${SRCTOP}/${hints}" ${DESTDIR}/boot/device.hints
469    fi
470
471    # Test /etc/rc - prints success and halts
472    cat > ${DESTDIR}/etc/rc <<'RCEOF'
473#!/bin/sh
474
475sysctl machdep.bootmethod
476echo "RC COMMAND RUNNING -- SUCCESS!!!!!"
477halt -p
478RCEOF
479    chmod +x ${DESTDIR}/etc/rc
480
481    # Create fstab used by UFS mtree overlays
482    cat > ${OUTDIR}/fstab.ufs <<EOF
483/dev/ufs/root	/	ufs	rw	1	1
484EOF
485    # Create fstab used by CD mtree overlays
486    cat > ${OUTDIR}/fstab.cd <<EOF
487/dev/iso9660/FBSDTEST	/	cd9660	ro	0	0
488EOF
489
490    echo "Boot tree built in ${DESTDIR}"
491}
492
493# --------------------------------------------------------------------------
494# Phase 2: Create base filesystem images
495#
496# Uses mtree overlays to vary the /boot/loader and /etc/fstab
497# without copying the tree. The base tree has no /boot/loader or
498# /etc/fstab; each image adds what it needs via an mtree spec file
499# passed as a second source to makefs.
500# --------------------------------------------------------------------------
501
502# Create a UFS image with a specific loader variant via mtree overlay
503make_one_ufs() {
504    variant=$1
505    img=${IMGDIR}/bootable-ufs-${variant}.img
506    mt=$(mktemp ${OUTDIR}/ufs-mtree.XXXXXX)
507
508    echo "  Creating UFS image with loader_${variant}..."
509    echo "./etc/fstab type=file mode=0644 contents=${OUTDIR}/fstab.ufs" > ${mt}
510    # BIOS: /boot/loader -> loader_<variant>
511    if [ -n "$(param bios_loaders)" ]; then
512	echo "./boot/loader type=file mode=0644 contents=${DESTDIR}/boot/loader_${variant}" >> ${mt}
513    fi
514    # OFW: boot1.hfs chainloads the single /boot/loader already in the
515    # tree (no per-variant overlay needed).
516    # EFI: /boot/loader.efi -> loader_<variant>.efi (needed for boot1.efi chainload)
517    if has efi; then
518	echo "./boot/loader.efi type=file mode=0755 contents=${DESTDIR}/boot/loader_${variant}.efi" >> ${mt}
519    fi
520    makefs -t ffs -B $(param byte_order) -M 10m -o label=root -o version=2 \
521	${img} ${mt} ${DESTDIR} >> ${LOGDIR}/imagebuild.log 2>&1
522    rm -f ${mt}
523}
524
525# Create a ZFS image with a specific loader variant via mtree overlay
526make_one_zfs() {
527    variant=$1
528    img=${IMGDIR}/bootable-zfs-${variant}.img
529    mt=$(mktemp ${OUTDIR}/zfs-mtree.XXXXXX)
530
531    echo "  Creating ZFS image with loader_${variant}..."
532    > ${mt}
533    # BIOS: /boot/loader -> loader_<variant>
534    if [ -n "$(param bios_loaders)" ]; then
535	echo "./boot/loader type=link link=loader_${variant}" >> ${mt}
536    fi
537    # OFW: boot1.hfs chainloads the single /boot/loader already in the
538    # tree (no per-variant overlay needed).
539    # EFI: /boot/loader.efi -> loader_<variant>.efi (needed for boot1.efi chainload)
540    if has efi; then
541	echo "./boot/loader.efi type=file mode=0755 contents=${DESTDIR}/boot/loader_${variant}.efi" >> ${mt}
542    fi
543    makefs -t zfs -s 100m \
544	-o poolname=ztestroot -o bootfs=ztestroot -o rootpath=/ \
545	${img} ${mt} ${DESTDIR} >> ${LOGDIR}/imagebuild.log 2>&1
546    rm -f ${mt}
547}
548
549# --------------------------------------------------------------------------
550# A/B (dual root) images
551#
552# gptboot.efi is the only FreeBSD EFI component that reads the GPT bootme
553# attribute, so it is the only way to test that the loader it chainloads
554# honours the partition that was selected for it, rather than picking a root
555# of its own.
556#
557# The failure being tested for is "boots fine, wrong root", not "fails to
558# boot": a loader that ignores the selected partition still reaches /etc/rc
559# and still looks healthy.  So the two roots have to differ in what they
560# print, and only the bootme one may print the string run_one_test greps for.
561# Two identical roots (what mkimg writes if handed the same image twice)
562# would make this test permanently green.
563# --------------------------------------------------------------------------
564
565# Create one of the two A/B root images.  ${1} is the slice letter: each slice
566# gets its own UFS label, an fstab pointing at that label, and an /etc/rc that
567# names the slice it booted from.  Only slice B -- the one set bootme in
568# assemble_efi_gpt_ab -- prints SUCCESS; booting A prints WRONG SLICE instead
569# and the test fails on timeout.
570make_one_ufs_ab() {
571    slice=$1
572    img=${IMGDIR}/bootable-ufs-ab-${slice}.img
573    mt=$(mktemp ${OUTDIR}/ufs-ab-mtree.XXXXXX)
574    fstab=${OUTDIR}/fstab.ufs.ab-${slice}
575    rc=${OUTDIR}/rc.ab-${slice}
576
577    echo "  Creating UFS image for A/B slice ${slice}..."
578    echo "/dev/ufs/root${slice}	/	ufs	rw	1	1" > ${fstab}
579
580    # Both roots report which one got mounted, so a failing log says what
581    # happened; only B counts as a pass.  vfs.root.mountfrom is the direct
582    # readout of the loader's choice.
583    if [ "${slice}" = "B" ]; then
584	verdict='echo "RC COMMAND RUNNING -- SUCCESS!!!!!"'
585    else
586	verdict='echo "WRONG SLICE -- booted A, expected B"'
587    fi
588    cat > ${rc} <<RCEOF
589#!/bin/sh
590
591sysctl machdep.bootmethod
592kenv vfs.root.mountfrom
593${verdict}
594halt -p
595RCEOF
596
597    echo "./etc/fstab type=file mode=0644 contents=${fstab}" > ${mt}
598    echo "./etc/rc type=file mode=0755 contents=${rc}" >> ${mt}
599    echo "./boot/loader.efi type=file mode=0755 contents=${DESTDIR}/boot/loader_lua.efi" >> ${mt}
600    makefs -t ffs -B $(param byte_order) -M 10m -o label=root${slice} -o version=2 \
601	${img} ${mt} ${DESTDIR} >> ${LOGDIR}/imagebuild.log 2>&1
602    rm -f ${mt}
603}
604
605# Create an ESP holding gptboot.efi rather than a loader variant.  gptboot.efi
606# is a boot1 flavour: it reads the GPT, picks the bootme partition, and
607# chainloads /boot/loader.efi from it.
608make_gptboot_esp() {
609    esp=${IMGDIR}/gptboot.esp
610    mt=$(mktemp ${OUTDIR}/esp-mtree.XXXXXX)
611
612    echo "  Creating ESP with gptboot.efi..."
613    cat > ${mt} <<EOF
614./efi type=dir uname=root gname=wheel mode=0755
615./efi/boot type=dir uname=root gname=wheel mode=0755
616./efi/boot/$(param efi_bootname).efi type=file uname=root gname=wheel mode=0755 contents=${DESTDIR}/boot/gptboot.efi
617EOF
618    makefs -t msdos \
619	-o fat_type=32 \
620	-o sectors_per_cluster=1 \
621	-o volume_label=EFISYS \
622	-s ${espsize}k \
623	${esp} ${mt} >> ${LOGDIR}/imagebuild.log 2>&1
624    rm -f ${mt}
625}
626
627# Create an ESP with the given EFI loader using an mtree spec
628make_one_esp() {
629    loader_name=$1
630    esp=${IMGDIR}/${loader_name}.esp
631    mt=$(mktemp ${OUTDIR}/esp-mtree.XXXXXX)
632
633    echo "  Creating ESP with ${loader_name}.efi..."
634    cat > ${mt} <<EOF
635./efi type=dir uname=root gname=wheel mode=0755
636./efi/boot type=dir uname=root gname=wheel mode=0755
637./efi/boot/$(param efi_bootname).efi type=file uname=root gname=wheel mode=0755 contents=${DESTDIR}/boot/${loader_name}.efi
638EOF
639    makefs -t msdos \
640	-o fat_type=32 \
641	-o sectors_per_cluster=1 \
642	-o volume_label=EFISYS \
643	-s ${espsize}k \
644	${esp} ${mt} >> ${LOGDIR}/imagebuild.log 2>&1
645    rm -f ${mt}
646}
647
648# Create a small ESP for CD hybrid boot using an mtree spec
649make_cd_esp() {
650    file=$1
651    loader=$2
652    mt=$(mktemp ${OUTDIR}/cd-esp-mtree.XXXXXX)
653
654    cat > ${mt} <<EOF
655./efi type=dir uname=root gname=wheel mode=0755
656./efi/boot type=dir uname=root gname=wheel mode=0755
657./efi/boot/$(param efi_bootname).efi type=file uname=root gname=wheel mode=0755 contents=${loader}
658EOF
659    makefs -t msdos \
660	-o fat_type=12 \
661	-o sectors_per_cluster=1 \
662	-o volume_label=EFISYS \
663	-s 2048k \
664	${file} ${mt} >> ${LOGDIR}/imagebuild.log 2>&1
665    rm -f ${mt}
666}
667
668# Find the pre-built Linux kernel EFI binary for linuxboot
669find_linux_kernel() {
670    target_arch=$(${MK} -v TARGET_ARCH)
671    linuxboot_dir=${ARCH_OBJDIR}/../../linuxboot/data/output
672
673    # amd64 has .efi suffix, others don't
674    for f in \
675	${linuxboot_dir}/${target_arch}.linux.v${LINUX_VERSION}.efi \
676	${linuxboot_dir}/${target_arch}.linux.v${LINUX_VERSION} \
677	${linuxboot_dir}/${target_arch}.v${LINUX_VERSION}.efi \
678	${linuxboot_dir}/${target_arch}.v${LINUX_VERSION}; do
679	if [ -f "$f" ]; then
680	    echo "$f"
681	    return
682	fi
683    done
684    return 1
685}
686
687# Build a linuxboot initrd using tar --format newc with an mtree spec.
688# No root/sudo required -- device nodes are written directly into the
689# cpio archive via mtree type=char entries.
690make_linuxboot_initrd() {
691    initrd=${IMGDIR}/linuxboot-initrd.cpio.gz
692    mt=$(mktemp ${OUTDIR}/initrd-mtree.XXXXXX)
693
694    echo "  Creating linuxboot initrd..."
695
696    # Build mtree spec for the initrd contents
697    cat > ${mt} <<EOF
698./init type=file mode=0755 contents=${DESTDIR}/boot/loader.kboot
699./dev type=dir mode=0755
700./dev/console type=char mode=0600 device=freebsd,5,0
701./dev/tty type=char mode=0600 device=freebsd,5,1
702./dev/ttyS0 type=char mode=0600 device=freebsd,4,64
703./boot type=dir mode=0755
704./boot/defaults type=dir mode=0755
705./boot/defaults/loader.conf type=file mode=0644 contents=${DESTDIR}/boot/defaults/loader.conf
706./boot/lua type=dir mode=0755
707EOF
708
709    # Add all lua files
710    for f in ${DESTDIR}/boot/lua/*.lua; do
711	[ -f "$f" ] || continue
712	bn=$(basename $f)
713	echo "./boot/lua/${bn} type=file mode=0644 contents=$f" >> ${mt}
714    done
715
716    # Add loader.help.kboot if present
717    if [ -f "${DESTDIR}/boot/loader.help.kboot" ]; then
718	echo "./boot/loader.help.kboot type=file mode=0644 contents=${DESTDIR}/boot/loader.help.kboot" >> ${mt}
719    fi
720
721    # Create the kboot-specific loader.conf
722    kboot_conf=$(mktemp ${OUTDIR}/kboot-loader-conf.XXXXXX)
723    cat > ${kboot_conf} <<EOF
724# Kboot configuration -- FreeBSD ${ARCH}
725boot_serial="YES"
726EOF
727    if [ "${ARCH}" = "amd64" ]; then
728	cat >> ${kboot_conf} <<EOF
729hw.uart.console="io:1016,br:115200"
730EOF
731    fi
732    echo "./boot/loader.conf type=file mode=0644 contents=${kboot_conf}" >> ${mt}
733
734    # Create the initrd as a gzip-compressed newc cpio archive
735    tar --format newc -cf - @${mt} 2>> ${LOGDIR}/imagebuild.log | \
736	gzip > ${initrd}
737
738    rm -f ${mt} ${kboot_conf}
739}
740
741# Build a linuxboot ESP containing the Linux kernel, initrd, and startup.nsh
742make_linuxboot_esp() {
743    linux_kernel=$1
744    esp=${IMGDIR}/linuxboot.esp
745    mt=$(mktemp ${OUTDIR}/linuxboot-esp-mtree.XXXXXX)
746
747    echo "  Creating linuxboot ESP..."
748
749    # Generate startup.nsh
750    startup=${OUTDIR}/startup.nsh
751    cat > ${startup} <<EOF
752\\linux.efi $(param linux_console) initrd=\\initrd
753EOF
754
755    cat > ${mt} <<EOF
756./startup.nsh type=file mode=0644 contents=${startup}
757./linux.efi type=file mode=0755 contents=${linux_kernel}
758./initrd type=file mode=0644 contents=${IMGDIR}/linuxboot-initrd.cpio.gz
759EOF
760    makefs -t msdos \
761	-o fat_type=32 \
762	-o sectors_per_cluster=1 \
763	-o volume_label=EFISYS \
764	-s 100m \
765	${esp} ${mt} >> ${LOGDIR}/imagebuild.log 2>&1
766    rm -f ${mt}
767}
768
769make_base_images() {
770    echo "=== Phase 2: Creating base filesystem images ==="
771
772    # UFS images - one per BIOS loader variant, plus one for EFI (uses lua)
773    if [ -n "$(param bios_loaders)" ]; then
774	for v in $(param bios_loaders); do
775	    make_one_ufs $v
776	done
777    else
778	# EFI-only arches still need one UFS image
779	make_one_ufs lua
780    fi
781
782    # ZFS images (if supported)
783    if has zfs; then
784	if [ -n "$(param bios_loaders)" ]; then
785	    for v in $(param bios_loaders); do
786		make_one_zfs $v
787	    done
788	else
789	    make_one_zfs lua
790	fi
791    fi
792
793    # ESP images (if EFI is supported)
794    if has efi; then
795	for l in $(param efi_loaders); do
796	    make_one_esp $l
797	done
798	# A/B: gptboot.efi on the ESP, plus two tellable-apart roots.
799	if [ -f "${DESTDIR}/boot/gptboot.efi" ]; then
800	    make_gptboot_esp
801	    make_one_ufs_ab A
802	    make_one_ufs_ab B
803	fi
804    fi
805
806    # Linuxboot initrd + ESP (if supported and Linux kernel is available)
807    if has linuxboot; then
808	linux_kernel=$(find_linux_kernel) || true
809	if [ -n "${linux_kernel}" ]; then
810	    make_linuxboot_initrd
811	    # EFI arches chainload the kernel+initrd off an ESP; platforms
812	    # without EFI hand them to QEMU directly (-kernel/-initrd), so
813	    # they need no ESP.
814	    has efi && make_linuxboot_esp ${linux_kernel}
815	else
816	    echo "  WARNING: Linux kernel not found for linuxboot, skipping"
817	    echo "    Expected in: ${ARCH_OBJDIR}/../../linuxboot/data/output/"
818	fi
819    fi
820
821    echo "Base images created in ${IMGDIR}"
822}
823
824# --------------------------------------------------------------------------
825# Phase 3: Assemble disk images and register tests
826# --------------------------------------------------------------------------
827
828# Test registration - uses temp files since /bin/sh doesn't have arrays.
829# ${TESTLIST} and ${OUTDIR} are set per-arch by setup_arch_env; the list is
830# truncated in build_all before each arch's images are (re)assembled.
831register_test() {
832    name=$1
833    shift
834    echo "$*" > ${OUTDIR}/test-cmd-${name}.sh
835    echo "${name}" >> ${TESTLIST}
836}
837
838# Like register_test, but for tests that need a real tap(4) interface
839# (assigned later by netboot_network_setup, once every arch is built) instead
840# of QEMU's slirp net.  The command is written out now with a placeholder tap
841# name; netboot_network_setup patches it in once the tap is assigned.
842# ${NETBOOT_PLAN} accumulates across every arch (unlike ${TESTLIST}, it is not
843# truncated per-arch), so it must already exist by the time build_all runs --
844# see Main.  $5 optionally names a different command builder than the default
845# qemu_netboot (e.g. qemu_netboot_ramdisk) -- it's always called as
846# "builder __NETBOOT_TAP__ ${fw}", so a non-default builder that doesn't need
847# ${fw} just ignores its second arg.
848register_netboot_test() {
849    name=$1
850    tftpdir=$2
851    bootfile=$3
852    fw=$4
853    builder=${5:-qemu_netboot}
854    cmdfile=${OUTDIR}/test-cmd-${name}.sh
855    echo "$(${builder} __NETBOOT_TAP__ ${fw})" > ${cmdfile}
856    echo "${name}" >> ${TESTLIST}
857    echo "${cmdfile} ${tftpdir} ${bootfile}" >> ${NETBOOT_PLAN}
858}
859
860assemble_efi_gpt() {
861    echo "  Assembling EFI+GPT images..."
862    for loader in $(param efi_loaders); do
863	esp=${IMGDIR}/${loader}.esp
864	fstypes="ufs"
865	has zfs && fstypes="ufs zfs"
866	for fs in ${fstypes}; do
867	    name="efi-gpt-${fs}-${loader}"
868	    img=${IMGDIR}/${name}.img
869
870	    # For EFI, the stage-3 in the filesystem doesn't matter, use lua variant
871	    case ${fs} in
872		ufs) fsimg=${IMGDIR}/bootable-ufs-lua.img; ptype="freebsd-ufs" ;;
873		zfs) fsimg=${IMGDIR}/bootable-zfs-lua.img; ptype="freebsd-zfs" ;;
874	    esac
875
876	    mkimg -s gpt \
877		-p efi:=${esp} \
878		-p ${ptype}:=${fsimg} \
879		-o ${img} >> ${LOGDIR}/imagebuild.log 2>&1
880
881	    register_test ${name} $(qemu_efi ${img})
882	done
883    done
884}
885
886# Build the A/B disk and register its test.  Layout mirrors an A/B upgrade
887# scheme: one ESP holding gptboot.efi, then two interchangeable root slices.
888#
889# bootme is set on p3 (slice B, the second root) on purpose.  With no bootme
890# anywhere gptboot.efi falls back to the first UFS partition, which is slice
891# A, so marking p2 instead would pass even if the attribute were ignored
892# entirely.  Marking the second root means only a loader that honours the
893# selected partition can reach slice B and print SUCCESS.
894assemble_efi_gpt_ab() {
895    echo "  Assembling EFI+GPT A/B image..."
896    name="efi-gpt-ufs-ab"
897    img=${IMGDIR}/${name}.img
898
899    mkimg -s gpt \
900	-p efi:=${IMGDIR}/gptboot.esp \
901	-p freebsd-ufs:=${IMGDIR}/bootable-ufs-ab-A.img \
902	-p freebsd-ufs:=${IMGDIR}/bootable-ufs-ab-B.img \
903	-o ${img} >> ${LOGDIR}/imagebuild.log 2>&1
904
905    ${GPTATTR} ${img} 3 ${GPT_ATTR_BOOTME} >> ${LOGDIR}/imagebuild.log 2>&1
906
907    register_test ${name} $(qemu_efi ${img})
908}
909
910assemble_efi_mbr() {
911    echo "  Assembling EFI+MBR images..."
912    for loader in $(param efi_loaders); do
913	esp=${IMGDIR}/${loader}.esp
914	name="efi-mbr-ufs-${loader}"
915	img=${IMGDIR}/${name}.img
916	ufs=${IMGDIR}/bootable-ufs-lua.img
917
918	mkimg -s bsd -p freebsd-ufs:=${ufs} -o ${img}.s2 >> ${LOGDIR}/imagebuild.log 2>&1
919	mkimg -a 1 -s mbr -p efi:=${esp} -p freebsd:=${img}.s2 -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1
920	rm -f ${img}.s2
921
922	register_test ${name} $(qemu_efi ${img})
923    done
924}
925
926assemble_bios_gpt() {
927    echo "  Assembling BIOS+GPT images..."
928    for variant in $(param bios_loaders); do
929	# UFS
930	name="bios-gpt-ufs-loader_${variant}"
931	img=${IMGDIR}/${name}.img
932	ufs=${IMGDIR}/bootable-ufs-${variant}.img
933
934	mkimg -s gpt -b ${DESTDIR}/boot/pmbr \
935	    -p freebsd-boot:=${DESTDIR}/boot/gptboot \
936	    -p freebsd-ufs:=${ufs} \
937	    -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1
938
939	register_test ${name} $(qemu_bios ${img})
940
941	# ZFS
942	if has zfs; then
943	    name="bios-gpt-zfs-loader_${variant}"
944	    img=${IMGDIR}/${name}.img
945	    zfs=${IMGDIR}/bootable-zfs-${variant}.img
946
947	    mkimg -s gpt -b ${DESTDIR}/boot/pmbr \
948		-p freebsd-boot:=${DESTDIR}/boot/gptzfsboot \
949		-p freebsd-zfs:=${zfs} \
950		-o ${img} >> ${LOGDIR}/imagebuild.log 2>&1
951
952	    register_test ${name} $(qemu_bios ${img})
953	fi
954    done
955}
956
957assemble_bios_mbr() {
958    echo "  Assembling BIOS+MBR images..."
959    for variant in $(param bios_loaders); do
960	name="bios-mbr-ufs-loader_${variant}"
961	img=${IMGDIR}/${name}.img
962	ufs=${IMGDIR}/bootable-ufs-${variant}.img
963
964	mkimg -s bsd -b ${DESTDIR}/boot/boot \
965	    -p freebsd-ufs:=${ufs} -o ${img}.s1 >> ${LOGDIR}/imagebuild.log 2>&1
966	# Note: boot0sio has a longish timeout, and does work but
967	# takes longer than 30s so we use mbr.
968	mkimg -a 1 -s mbr -b ${DESTDIR}/boot/mbr \
969	    -p freebsd:=${img}.s1 -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1
970	rm -f ${img}.s1
971
972	register_test ${name} $(qemu_bios ${img})
973    done
974}
975
976# Hybrid GPT images: ESP + freebsd-boot + filesystem, tested with both BIOS and EFI
977assemble_both_gpt() {
978    echo "  Assembling hybrid BIOS+EFI GPT images..."
979    for l in $(param bios_loaders); do
980	loader="loader_${l}"
981	esp=${IMGDIR}/${loader}.esp
982	fstypes="ufs"
983	has zfs && fstypes="ufs zfs"
984	for fs in ${fstypes}; do
985	    name_base="both-gpt-${fs}-${loader}"
986	    img=${IMGDIR}/${name_base}.img
987
988	    case ${fs} in
989		ufs)
990		    fsimg=${IMGDIR}/bootable-ufs-lua.img
991		    ptype="freebsd-ufs"
992		    bootblk=${DESTDIR}/boot/gptboot
993		    ;;
994		zfs)
995		    fsimg=${IMGDIR}/bootable-zfs-lua.img
996		    ptype="freebsd-zfs"
997		    bootblk=${DESTDIR}/boot/gptzfsboot
998		    ;;
999	    esac
1000
1001	    mkimg -b ${DESTDIR}/boot/pmbr -s gpt \
1002		-p efi:=${esp} \
1003		-p freebsd-boot:=${bootblk} \
1004		-p ${ptype}:=${fsimg} \
1005		-o ${img} >> ${LOGDIR}/imagebuild.log 2>&1
1006
1007	    register_test "${name_base}-bios" $(qemu_bios ${img})
1008	    register_test "${name_base}-efi" $(qemu_efi ${img})
1009	done
1010    done
1011}
1012
1013# Hybrid MBR images: ESP + freebsd(boot+ufs), tested with both BIOS and EFI
1014assemble_both_mbr() {
1015    echo "  Assembling hybrid BIOS+EFI MBR images..."
1016    for l in $(param bios_loaders); do
1017	loader="loader_${l}"
1018	esp=${IMGDIR}/${loader}.esp
1019	name_base="both-mbr-ufs-${loader}"
1020	img=${IMGDIR}/${name_base}.img
1021	ufs=${IMGDIR}/bootable-ufs-lua.img
1022
1023	mkimg -s bsd -b ${DESTDIR}/boot/boot \
1024	    -p freebsd-ufs:=${ufs} -o ${img}.s2 >> ${LOGDIR}/imagebuild.log 2>&1
1025	mkimg -a 2 -s mbr -b ${DESTDIR}/boot/mbr \
1026	    -p efi:=${esp} \
1027	    -p freebsd:=${img}.s2 -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1
1028	rm -f ${img}.s2
1029
1030	register_test "${name_base}-bios" $(qemu_bios ${img})
1031	register_test "${name_base}-efi" $(qemu_efi ${img})
1032    done
1033}
1034
1035assemble_cd() {
1036    echo "  Assembling CD images..."
1037
1038    # mtree to add loader and fstab to image
1039    mt1=$(mktemp ${OUTDIR}/cd-mtree.XXXXXX)
1040    variant=lua
1041    cat > ${mt1} <<EOF
1042./boot/loader type=file mode=0644 contents=${DESTDIR}/boot/loader_${variant}
1043./etc/fstab type=file mode=0644 contents=${OUTDIR}/fstab.cd
1044EOF
1045
1046    # cdboot - BIOS CD boot via El Torito
1047    name="bios-cd-cdboot"
1048    img=${IMGDIR}/${name}.iso
1049    makefs -t cd9660 \
1050	-o bootimage=i386\;${DESTDIR}/boot/cdboot \
1051	-o no-emul-boot \
1052	-o rockridge \
1053	-o label=FBSDTEST \
1054	${img} ${mt1} ${DESTDIR} >> ${LOGDIR}/imagebuild.log 2>&1
1055    register_test ${name} $(qemu_bios_cdrom ${img})
1056
1057    # isoboot - hybrid BIOS+EFI CD (tests isoboot for BIOS, loader.efi for EFI)
1058    name="hybrid-cd-isoboot"
1059    img=${IMGDIR}/${name}.iso
1060    espfile=$(mktemp ${OUTDIR}/efiboot.XXXXXX)
1061    make_cd_esp ${espfile} ${DESTDIR}/boot/loader_lua.efi
1062
1063    makefs -t cd9660 \
1064	-o bootimage=i386\;${DESTDIR}/boot/cdboot \
1065	-o no-emul-boot \
1066	-o bootimage=i386\;${espfile} \
1067	-o no-emul-boot \
1068	-o platformid=efi \
1069	-o rockridge \
1070	-o label=FBSDTEST \
1071	${img} ${mt1} ${DESTDIR} >> ${LOGDIR}/imagebuild.log 2>&1
1072
1073    # Overlay hybrid GPT for isoboot
1074    imgsize=$(stat -f %z "${img}")
1075
1076    # Find the EFI partition in the ISO to reference it
1077    espstart=""
1078    espsize_cd=""
1079    for entry in $(etdump --format shell ${img}); do
1080	eval ${entry}
1081	if [ "${et_platform}" = "efi" ]; then
1082	    espstart=$(expr ${et_lba} \* 2048)
1083	    espsize_cd=$(expr ${et_sectors} \* 512)
1084	    break
1085	fi
1086    done
1087
1088    if [ -n "${espstart}" ]; then
1089	hybrid=$(mktemp ${OUTDIR}/hybrid.XXXXXX)
1090	mkimg -s gpt \
1091	    --capacity ${imgsize} \
1092	    -b ${DESTDIR}/boot/pmbr \
1093	    -p freebsd-boot:=${DESTDIR}/boot/isoboot \
1094	    -p efi::${espsize_cd}:${espstart} \
1095	    -o ${hybrid} >> ${LOGDIR}/imagebuild.log 2>&1
1096	dd if=${hybrid} of=${img} bs=32k count=1 conv=notrunc >> ${LOGDIR}/imagebuild.log 2>&1
1097	rm -f ${hybrid}
1098    fi
1099
1100    rm -f ${espfile}
1101
1102    # Test the hybrid ISO with both BIOS and EFI
1103    register_test "${name}-bios" $(qemu_bios_cdrom ${img})
1104    register_test "${name}-efi" $(qemu_efi_cdrom ${img})
1105
1106    rm -f ${mt1}
1107}
1108
1109assemble_ofw() {
1110    echo "  Assembling Open Firmware images..."
1111    # APM partitioned disk with boot1.hfs
1112    fstypes="ufs"
1113    has zfs && fstypes="ufs zfs"
1114    for fs in ${fstypes}; do
1115	name="ofw-apm-${fs}"
1116	img=${IMGDIR}/${name}.img
1117
1118	case ${fs} in
1119	    ufs) fsimg=${IMGDIR}/bootable-ufs-lua.img; ptype="freebsd-ufs" ;;
1120	    zfs) fsimg=${IMGDIR}/bootable-zfs-lua.img; ptype="freebsd-zfs" ;;
1121	esac
1122
1123	mkimg -a 1 -s apm \
1124	    -p freebsd-boot:=${DESTDIR}/boot/boot1.hfs \
1125	    -p ${ptype}:=${fsimg} \
1126	    -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1
1127
1128	register_test ${name} $(qemu_ofw ${img})
1129    done
1130}
1131
1132# Open Firmware bootable CD.  Mirrors release/powerpc/mkisoimages.sh: build the
1133# Apple/OF "macppc" boot image by dd'ing /boot/loader into the hfs-boot block
1134# at its "Loader START" offset, then hand that to makefs as the El Torito
1135# no-emul boot image.  OpenBIOS finds it via cd:,\\:tbxi.
1136assemble_ofw_cd() {
1137    echo "  Assembling Open Firmware CD image..."
1138    name="ofw-cd"
1139    img=${IMGDIR}/${name}.iso
1140
1141    # cd9660 root fstab overlay
1142    mt=$(mktemp ${OUTDIR}/ofwcd-mtree.XXXXXX)
1143    echo "./etc/fstab type=file mode=0644 contents=${OUTDIR}/fstab.cd" > ${mt}
1144
1145    # Apple/OF boot block with the loader embedded at "Loader START".
1146    bootblock=$(mktemp ${OUTDIR}/hfs-boot.XXXXXX)
1147    uudecode -p ${SRCTOP}/release/powerpc/hfs-boot.bz2.uu | bunzip2 > ${bootblock}
1148    offset=$(hd ${bootblock} | grep 'Loader START' | cut -f 1 -d ' ')
1149    offset=$((0x${offset} / 512))
1150    dd if=${DESTDIR}/boot/loader of=${bootblock} seek=${offset} conv=notrunc \
1151	>> ${LOGDIR}/imagebuild.log 2>&1
1152
1153    makefs -t cd9660 \
1154	-o bootimage=macppc\;${bootblock} \
1155	-o no-emul-boot \
1156	-o rockridge \
1157	-o label=FBSDTEST \
1158	${img} ${mt} ${DESTDIR} >> ${LOGDIR}/imagebuild.log 2>&1
1159
1160    rm -f ${bootblock} ${mt}
1161    register_test ${name} $(qemu_ofw_cdrom ${img})
1162}
1163
1164# pseries PReP disk boot: MBR with a PReP boot partition (boot1.elf) and the
1165# UFS root directly on an MBR partition -- no BSD label, matching freebsd-ci
1166# (whose BSD-slice container does not cross-build from amd64).  SLOF runs
1167# boot1.elf from the PReP partition, which loads /boot/loader from the UFS.
1168assemble_prep() {
1169    echo "  Assembling pseries PReP (MBR) images..."
1170    name="prep-mbr-ufs"
1171    img=${IMGDIR}/${name}.img
1172    ufs=${IMGDIR}/bootable-ufs-lua.img
1173
1174    mkimg -a 1 -s mbr \
1175	-p prepboot:=${DESTDIR}/boot/boot1.elf \
1176	-p freebsd:=${ufs} \
1177	-o ${img} >> ${LOGDIR}/imagebuild.log 2>&1
1178
1179    register_test ${name} $(qemu_prep ${img})
1180}
1181
1182# pseries CD boot.  SLOF reads \ppc\bootinfo.txt (CHRP boot script) and runs
1183# the OF loader from the CD.  Mirrors the chrp-boot half of
1184# release/powerpc/mkisoimages.sh (the macppc/Apple half is mac99-only).
1185assemble_pseries_cd() {
1186    echo "  Assembling pseries CD image..."
1187    name="prep-cd"
1188    img=${IMGDIR}/${name}.iso
1189
1190    mt=$(mktemp ${OUTDIR}/pseriescd-mtree.XXXXXX)
1191    bootinfo=$(mktemp ${OUTDIR}/bootinfo.XXXXXX)
1192    cat > ${bootinfo} <<EOF
1193<chrp-boot>
1194<description>FreeBSD Install</description>
1195<os-name>FreeBSD</os-name>
1196<boot-script>boot &device;:,\ppc\chrp\loader</boot-script>
1197</chrp-boot>
1198EOF
1199    cat > ${mt} <<EOF
1200./etc/fstab type=file mode=0644 contents=${OUTDIR}/fstab.cd
1201./ppc type=dir mode=0755
1202./ppc/bootinfo.txt type=file mode=0644 contents=${bootinfo}
1203./ppc/chrp type=dir mode=0755
1204./ppc/chrp/loader type=file mode=0644 contents=${DESTDIR}/boot/loader
1205EOF
1206    makefs -t cd9660 \
1207	-o chrp-boot \
1208	-o rockridge \
1209	-o label=FBSDTEST \
1210	${img} ${mt} ${DESTDIR} >> ${LOGDIR}/imagebuild.log 2>&1
1211    rm -f ${mt} ${bootinfo}
1212    register_test ${name} $(qemu_prep_cdrom ${img})
1213}
1214
1215assemble_linuxboot() {
1216    echo "  Assembling linuxboot images..."
1217    esp=${IMGDIR}/linuxboot.esp
1218    fstypes="ufs"
1219    has zfs && fstypes="ufs zfs"
1220    for fs in ${fstypes}; do
1221	name="linuxboot-gpt-${fs}"
1222	img=${IMGDIR}/${name}.img
1223
1224	case ${fs} in
1225	    ufs) fsimg=${IMGDIR}/bootable-ufs-lua.img; ptype="freebsd-ufs" ;;
1226	    zfs) fsimg=${IMGDIR}/bootable-zfs-lua.img; ptype="freebsd-zfs" ;;
1227	esac
1228
1229	mkimg -s gpt \
1230	    -p efi:=${esp} \
1231	    -p ${ptype}:=${fsimg} \
1232	    -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1
1233
1234	register_test ${name} $(qemu_efi ${img})
1235    done
1236}
1237
1238# Linuxboot for platforms without an ESP: the kernel+initrd go on the QEMU
1239# command line, and the disk is just the FreeBSD root filesystem in a GPT
1240# (no ESP partition).  loader.kboot mounts root from that disk.
1241assemble_linuxboot_direct() {
1242    echo "  Assembling linuxboot (direct kernel/initrd) images..."
1243    linux_kernel=$(find_linux_kernel) || return 0
1244    initrd=${IMGDIR}/linuxboot-initrd.cpio.gz
1245    fstypes="ufs"
1246    has zfs && fstypes="ufs zfs"
1247    for fs in ${fstypes}; do
1248	name="linuxboot-${fs}"
1249	img=${IMGDIR}/${name}.img
1250
1251	case ${fs} in
1252	    ufs) fsimg=${IMGDIR}/bootable-ufs-lua.img; ptype="freebsd-ufs" ;;
1253	    zfs) fsimg=${IMGDIR}/bootable-zfs-lua.img; ptype="freebsd-zfs" ;;
1254	esac
1255
1256	mkimg -s gpt \
1257	    -p ${ptype}:=${fsimg} \
1258	    -o ${img} >> ${LOGDIR}/imagebuild.log 2>&1
1259
1260	register_test ${name} $(qemu_linuxboot ${img} ${linux_kernel} ${initrd})
1261    done
1262}
1263
1264# Netboot: stage a TFTP root and register PXE/EFI network-boot tests.  The
1265# loader fetches the kernel + an md_image RAM root over TFTP and mounts
1266# /dev/md0.  The bootable UFS image is reused as the RAM root, so the kernel
1267# must have "options MD_ROOT" (amd64/aarch64 do; riscv64/armv7 GENERIC do
1268# not -- see the plan).  netboot-bios/netboot-efi go over a real tap(4) +
1269# dnsmasq (register_netboot_test / netboot_network_setup) so DHCP can carry a
1270# root-path.
1271assemble_netboot() {
1272    echo "  Assembling netboot images..."
1273    tftp=${OUTDIR}/tftp
1274    rm -rf ${tftp}
1275    mkdir -p ${tftp}
1276    cp -a ${DESTDIR}/boot ${tftp}/boot
1277    cp ${IMGDIR}/bootable-ufs-lua.img ${tftp}/boot/mdroot.img
1278
1279    # Serve the kernel and RAM root as .xz: xzfs_fsops (stand/libsa/xzfs.c)
1280    # transparently retries "<name>.xz" whenever "<name>" isn't found,
1281    # exactly like gzipfs already does for kernel.gz, so this cuts the bytes
1282    # that have to cross TFTP with no loader.conf change -- mdroot_name below
1283    # still names the plain, unsuffixed "mdroot.img", and pxeboot/loader.efi
1284    # (fetched directly by firmware, not through libsa) are untouched. Both
1285    # the amd64 BIOS loader (i386/loader/Makefile: LOADER_XZ_SUPPORT) and
1286    # every EFI loader (efi/loader/conf.c: unconditional) have xzfs compiled
1287    # in, so netboot-bios and netboot-efi -- which share this tftp tree --
1288    # both exercise the compressed path.
1289    #
1290    # -6 (xz's own default), not -9: -9's 64MB LZMA2 dictionary needs one
1291    # contiguous vmalloc() of that size before xz_dec_run() can decode
1292    # anything, and the BIOS loader's *entire* heap is a fixed 64MB
1293    # (HEAP_MIN in i386/libi386/biosmem.c) regardless of VM memory -- so a
1294    # 64MB dictionary request against a 64MB heap that already holds other
1295    # loader state can never succeed (XZ_MEM_ERROR, every read, forever).
1296    # -6's 8MB dictionary leaves ample headroom and loses ~nothing on data
1297    # this size.
1298    xz -6 -f ${tftp}/boot/kernel/kernel
1299    xz -6 -f ${tftp}/boot/mdroot.img
1300
1301    # Boot the kernel + RAM root off the network instead of a disk (appended to
1302    # the arch-correct loader.conf build_tree already wrote).
1303    cat >> ${tftp}/boot/loader.conf <<EOF
1304mdroot_load="YES"
1305mdroot_type="md_image"
1306mdroot_name="/boot/mdroot.img"
1307vfs.root.mountfrom="ufs:/dev/md0"
1308EOF
1309
1310    # BIOS PXE: the NIC option ROM chainloads pxeboot (the DHCP bootfile).
1311    # Registered via register_netboot_test, not register_test: these need a
1312    # real tap(4) + dnsmasq root-path (see netboot_network_setup) rather than
1313    # slirp, which can't send one.
1314    #
1315    # netboot-bios legitimately runs past the default timeout: it's TFTP
1316    # (not xzfs decode) that's the bottleneck here -- EFI's memdisk=<url>
1317    # path decodes 150MB .xz images in about a minute, so the loader's xz
1318    # decoder isn't the bottleneck; this is plain TFTP bandwidth for the
1319    # kernel+mdroot fetch. Measured wins on real hardware: whole-suite boot
1320    # time dropped from ~25 min to ~18 min with xzfs in place. TFTP-level
1321    # fixes (e.g. HTTP instead, once BIOS can do that) are the only further
1322    # lever; don't chase this again without one.
1323    if has bios; then
1324	cp ${DESTDIR}/boot/pxeboot ${tftp}/pxeboot
1325	register_netboot_test netboot-bios ${tftp} pxeboot bios
1326    fi
1327    # EFI: stage loader.efi for the iPXE-chainload test below.  This can't use
1328    # the NIC's own PXE ROM the way netboot-bios does -- our installed OVMF
1329    # build (qemu's bundled edk2-x86_64-code.fd) has no NetworkPkg/PXE driver
1330    # at all (confirmed: zero PXE/HTTPBoot strings in the firmware, and
1331    # BdsDxe drops straight to "EFI Internal Shell" with no network boot
1332    # option ever offered, independent of vars pflash/bootindex/-boot n). So
1333    # EFI netboot instead brings its own iPXE (see the ramdisk block below)
1334    # to do DHCP/TFTP; netboot-efi's own chain script (also below) omits
1335    # memdisk= so loader.efi does its own BOOTP call and gets a real
1336    # DHCP root-path, unlike the RAM-disk tests.
1337    if has efi; then
1338	cp ${DESTDIR}/boot/loader_lua.efi ${tftp}/loader.efi
1339    fi
1340
1341    # RAM-disk netboot: iPXE (from -hda) chains loader.efi with memdisk=<url>,
1342    # which the loader boots entirely from RAM -- sidestepping the NFS/root-path
1343    # problem above.  iPXE EFI is x86-only in the install, so this is gated on
1344    # netboot_ipxe (amd64 today).  See qemu_netboot_ramdisk / do-memddisk-efi.
1345    # netboot-efi (real root-path, no memdisk=) piggybacks on the same iPXE
1346    # image/vars, since it needs the same OVMF-has-no-PXE workaround.
1347    if [ -n "$(param netboot_ipxe)" ] && have_file "$(param netboot_ipxe)" && \
1348	    have_file "$(param netboot_efi_vars)"; then
1349	# Writable copies: QEMU opens -hda and the edk2 vars pflash read-write,
1350	# but the installed originals are root-owned.
1351	cp -f $(param netboot_ipxe) ${OUTDIR}/netboot-ipxe.img
1352	cp -f $(param netboot_efi_vars) ${OUTDIR}/netboot-vars.fd
1353
1354	if has efi; then
1355	    cat > ${tftp}/boot-efi.ipxe <<EOF
1356#!ipxe
1357chain tftp://\${next-server}/loader.efi
1358EOF
1359	    register_netboot_test netboot-efi ${tftp} /boot-efi.ipxe "" qemu_netboot_ramdisk
1360
1361	    # Same idea as netboot-efi, but rootdev= forces currdev to an
1362	    # http:// URL so the root fetch exercises the loader's own EFI
1363	    # HTTP devspec (efi/libefi/efihttp.c) instead of tftp/NFS.
1364	    cat > ${tftp}/boot-http.ipxe <<EOF
1365#!ipxe
1366chain tftp://\${next-server}/loader.efi rootdev=http://\${next-server}/
1367EOF
1368	    register_netboot_test netboot-http-efi ${tftp} /boot-http.ipxe "" qemu_netboot_ramdisk
1369	fi
1370
1371	cat > ${tftp}/boot.ipxe <<EOF
1372#!ipxe
1373chain tftp://\${next-server}/loader.efi memdisk=tftp://\${next-server}/boot/mdroot.img
1374EOF
1375	register_netboot_test netboot-ramdisk ${tftp} /boot.ipxe "" qemu_netboot_ramdisk
1376
1377	# Same mechanism, but the image is compressed, exercising each codec
1378	# in stand/efi/loader/decompress.c.  Compress a throwaway copy rather
1379	# than ${IMGDIR}/bootable-ufs-lua.img itself: gzip/bzip2/xz remove
1380	# their input on success, and this image is shared with other tests.
1381	# Compressing a real file (not a pipe) lets zstd embed the frame
1382	# content size, so zstd_init() can size the output buffer exactly
1383	# instead of guessing 4x and growing/copying as it decompresses.
1384	for spec in gzip:gz bzip2:bz2 xz:xz zstd:zst; do
1385	    tool=${spec%%:*}
1386	    ext=${spec#*:}
1387	    img=${tftp}/boot/mdroot-${tool}.img
1388	    cp ${IMGDIR}/bootable-ufs-lua.img ${img}
1389	    case ${tool} in
1390		zstd) zstd -f --rm ${img} >> ${LOGDIR}/imagebuild.log 2>&1 ;;
1391		*)    ${tool} -f ${img} >> ${LOGDIR}/imagebuild.log 2>&1 ;;
1392	    esac
1393	    cat > ${tftp}/boot-${tool}.ipxe <<EOF
1394#!ipxe
1395chain tftp://\${next-server}/loader.efi memdisk=tftp://\${next-server}/boot/mdroot-${tool}.img.${ext}
1396EOF
1397	    register_netboot_test netboot-ramdisk-${tool} ${tftp} /boot-${tool}.ipxe "" qemu_netboot_ramdisk
1398	done
1399    fi
1400
1401    # BIOS RAM-disk netboot: iPXE loads syslinux memdisk with a *bootable* disk
1402    # image as its initrd.  memdisk boots the image's MBR and installs a MEMDISK
1403    # memory disk; the BIOS loader detects it (biosmemdisk.c -> hint.md.0.*), so
1404    # the kernel gets md0 and roots via the UFS label.  The BIOS loader has no
1405    # memdisk= arg, hence this different mechanism.
1406    if has bios && [ -n "$(param netboot_memdisk)" ] && \
1407	    have_file "$(param netboot_memdisk)"; then
1408	cp $(param netboot_memdisk) ${tftp}/memdisk
1409	cp ${IMGDIR}/bios-mbr-ufs-loader_lua.img ${tftp}/bootdisk.img
1410	cat > ${tftp}/boot-bios.ipxe <<EOF
1411#!ipxe
1412kernel tftp://\${next-server}/memdisk
1413initrd tftp://\${next-server}/bootdisk.img
1414boot
1415EOF
1416	register_netboot_test netboot-bios-memdisk ${tftp} /boot-bios.ipxe bios
1417    fi
1418}
1419
1420# --------------------------------------------------------------------------
1421# Netboot host networking: vmnet(4) + dnsmasq
1422#
1423# QEMU's slirp/user-mode net cannot send DHCP root-path (option 17); without
1424# one, the loader's netproto defaults to NFS for every file fetch after the
1425# initial TFTP-delivered bootfile (net_parse_rootpath()/NETPROTO_DEFAULT), which
1426# is why netboot-bios/ netboot-efi otherwise fail here (no NFS server).
1427#
1428# This uses vmnet(4), not tap(4), even though both are clones of the same
1429# if_tuntap(4) driver and QEMU treats them identically (-netdev tap,ifname=).
1430# tap(4)'s close handler unconditionally runs if_down()+if_purgeaddrs() on last
1431# close -- so the interface would lose its address and go down every time QEMU
1432# exits, and re-adding it needs root. vmnet(4) explicitly skips that, so the
1433# address assigned once survives every subsequent QEMU open/close.
1434#
1435# Creating the interface and giving it an address both require root regardless
1436# of net.link.tap.user_open (that sysctl only gates opening the cloning device
1437# itself). So the root setup below runs once, chowns the resulting /dev/vmnetN
1438# nodes to the invoking user (so QEMU, unprivileged, can open them directly --
1439# see net/tap-bsd.c: it opens /dev/<ifname> directly when that device already
1440# exists, only falling back to the /dev/tap cloning device otherwise), and
1441# leaves everything running. Later invocations detect the existing state via a
1442# content hash and skip sudo entirely: "once per boot", not "once per run".
1443# --------------------------------------------------------------------------
1444
1445# Deterministically slice a /30 out of $(param netboot_subnet_base) (a /16,
1446# e.g. 198.18.0.0) for plan line ${1} (0-based). Prints "gw client".
1447netboot_subnet_for() {
1448    idx=$1
1449    base=$(param netboot_subnet_base)
1450    o1o2=$(echo ${base} | cut -d. -f1-2)
1451    o3=$(($(echo ${base} | cut -d. -f3) + idx / 64))
1452    o4=$(((idx % 64) * 4))
1453    echo "${o1o2}.${o3}.$((o4 + 1)) ${o1o2}.${o3}.$((o4 + 2))"
1454}
1455
1456# Runs after every arch is built (so ${NETBOOT_PLAN} is complete) and before
1457# run_all_tests. Assigns each registered netboot test its own vmnet + /30,
1458# escalates via sudo only if the host doesn't already match that plan, then
1459# patches the __NETBOOT_TAP__ placeholder in each test's command file.
1460netboot_network_setup() {
1461    [ -s "${NETBOOT_PLAN}" ] || return 0
1462    echo "=== Netboot: configuring vmnet(4) + dnsmasq ==="
1463    mkdir -p "${NETBOOT_STATE_DIR}"
1464
1465    resolved=${NETBOOT_STATE_DIR}/plan.new
1466    : > "${resolved}"
1467    idx=0
1468    while read cmdfile tftpdir bootfile; do
1469	vmnet="vmnet$((100 + idx))"	# offset away from any operator-managed vmnets
1470	set -- $(netboot_subnet_for ${idx})
1471	gw=$1
1472	client=$2
1473	echo "${vmnet} ${gw} ${client} 30 ${tftpdir} ${bootfile} ${cmdfile}" >> "${resolved}"
1474	idx=$((idx + 1))
1475    done < "${NETBOOT_PLAN}"
1476
1477    newhash=$(md5 -q "${resolved}")
1478    oldhash=""
1479    [ -f "${NETBOOT_STATE_DIR}/state.hash" ] && oldhash=$(cat "${NETBOOT_STATE_DIR}/state.hash")
1480
1481    # dnsmasq runs as "nobody" (it drops root after binding), so kill -0
1482    # from this unprivileged process would always fail with EPERM even when
1483    # it's alive; check process existence via ps instead, which doesn't
1484    # require signal permission.
1485    converged=false
1486    if [ "${newhash}" = "${oldhash}" ] && [ -s "${NETBOOT_STATE_DIR}/dnsmasq.pid" ] \
1487	    && ps -p "$(cat ${NETBOOT_STATE_DIR}/dnsmasq.pid)" > /dev/null 2>&1; then
1488	converged=true
1489	while read vmnet gw client prefix tftpdir bootfile cmdfile; do
1490	    ifconfig "${vmnet}" > /dev/null 2>&1 || { converged=false; break; }
1491	done < "${resolved}"
1492    fi
1493
1494    # Each vmnet also gets its own python3 http.server (see netboot_helper),
1495    # so http:// fetches (netboot-http-efi) have something to hit at
1496    # ${next-server} alongside dnsmasq's tftp-root. Confirm the pid count
1497    # still matches the plan and every one of them is still alive.
1498    if ${converged}; then
1499	if [ -s "${NETBOOT_STATE_DIR}/httpd.pids" ] && \
1500		[ "$(wc -l < "${NETBOOT_STATE_DIR}/httpd.pids")" -eq "$(wc -l < "${resolved}")" ]; then
1501	    while read pid; do
1502		ps -p "${pid}" > /dev/null 2>&1 || { converged=false; break; }
1503	    done < "${NETBOOT_STATE_DIR}/httpd.pids"
1504	else
1505	    converged=false
1506	fi
1507    fi
1508
1509    if ${converged}; then
1510	echo "  Existing vmnet(4)/dnsmasq setup already matches -- no sudo needed."
1511    else
1512	echo "  Host networking missing or stale; requesting sudo once to (re)create it..."
1513	cp "${resolved}" "${NETBOOT_STATE_DIR}/plan"
1514	sudo "$0" --netboot-helper "${NETBOOT_STATE_DIR}/plan" \
1515	    || die "netboot vmnet(4)/dnsmasq setup (sudo) failed"
1516    fi
1517
1518    while read vmnet gw client prefix tftpdir bootfile cmdfile; do
1519	sed -i '' "s/__NETBOOT_TAP__/${vmnet}/" "${cmdfile}"
1520    done < "${resolved}"
1521}
1522
1523# Root-side setup, only reached via `sudo "$0" --netboot-helper <planfile>`
1524# (see netboot_network_setup). ${1} has the same "vmnet gw client prefix
1525# tftpdir bootfile cmdfile" lines as netboot_network_setup's resolved plan.
1526netboot_helper() {
1527    planfile=$1
1528    [ -r "${planfile}" ] || die "netboot helper: cannot read plan ${planfile}"
1529    need_cmd python3
1530    invoker=${SUDO_UID:-$(id -u)}
1531    mkdir -p "${NETBOOT_STATE_DIR}"
1532
1533    # Converge from scratch rather than trusting the caller's hash check:
1534    # tear down anything left over from a previous (possibly interrupted) run.
1535    if [ -s "${NETBOOT_STATE_DIR}/dnsmasq.pid" ]; then
1536	kill "$(cat ${NETBOOT_STATE_DIR}/dnsmasq.pid)" 2>/dev/null || true
1537	rm -f "${NETBOOT_STATE_DIR}/dnsmasq.pid"
1538    fi
1539    if [ -s "${NETBOOT_STATE_DIR}/httpd.pids" ]; then
1540	while read pid; do
1541	    kill "${pid}" 2>/dev/null || true
1542	done < "${NETBOOT_STATE_DIR}/httpd.pids"
1543    fi
1544    : > "${NETBOOT_STATE_DIR}/httpd.pids"
1545    for vmnet in $(ifconfig -g boot-test 2>/dev/null); do
1546	ifconfig "${vmnet}" destroy
1547    done
1548
1549    conf=${NETBOOT_STATE_DIR}/dnsmasq.conf
1550    cat > "${conf}" <<EOF
1551port=0
1552bind-interfaces
1553enable-tftp
1554log-dhcp
1555pid-file=${NETBOOT_STATE_DIR}/dnsmasq.pid
1556dhcp-leasefile=${NETBOOT_STATE_DIR}/dnsmasq.leases
1557
1558# QEMU's PXE ROM is iPXE; it self-identifies as vendor-class
1559# "PXEClient:Arch:00000:UNDI:002001" (stand/libsa/bootp.c's own request just
1560# says "PXEClient", no ":Arch:..." suffix, so this substring match is
1561# iPXE-only). iPXE's autoboot() gives DHCP root-path priority over
1562# chainloading the DHCP filename -- if root-path is set it tries to
1563# sanboot(8) it instead, which fails outright for a tftp:// URI ("Could not
1564# open SAN device"). So root-path below is withheld from iPXE's own
1565# negotiation and only given once stand/libsa/bootp.c does its own separate
1566# BOOTP call after pxeboot/loader.efi has already been chainloaded.
1567dhcp-vendorclass=set:ipxerom,PXEClient:Arch
1568EOF
1569
1570    while read vmnet gw client prefix tftpdir bootfile cmdfile; do
1571	ifconfig "${vmnet}" create group boot-test
1572	ifconfig "${vmnet}" inet "${gw}/${prefix}" up
1573	chown "${invoker}" "/dev/${vmnet}"
1574
1575	# One http.server per subnet, bound only to that subnet's own gw
1576	# address (not 0.0.0.0), so concurrently-running tests never collide
1577	# on port 80. Serves the exact same tree as dnsmasq's tftp-root above
1578	# -- http:// and tftp:// fetches of the same path see identical bytes.
1579	# Harmless for tests that never issue an http:// fetch; only
1580	# netboot-http-efi actually relies on it today.
1581	python3 -m http.server 80 --bind "${gw}" --directory "${tftpdir}" \
1582	    > "${NETBOOT_STATE_DIR}/httpd-${vmnet}.log" 2>&1 &
1583	echo $! >> "${NETBOOT_STATE_DIR}/httpd.pids"
1584
1585	# dnsmasq's dhcp-boot only sets DHCP option 67 (bootfile-name), never
1586	# the classic fixed-length BOOTP "file" field -- confirmed by comparing
1587	# against QEMU's own slirp DHCP server, which sets that field directly
1588	# and gets a plain "Filename: pxeboot" chainload with no further
1589	# ceremony. Without it, and since iPXE requested option 43 (PXE vendor
1590	# info) in its Parameter-Request list, iPXE assumes it must run the full
1591	# PXE boot-server-discovery dance instead of trusting option 67 -- seen
1592	# on the wire as a TFTP RRQ with an empty filename, then "No
1593	# configuration methods succeeded" / "PXEBS ...  Connection timed
1594	# out". Telling it not to via PXE discovery-control (option 43
1595	# sub-option 6, value 8 = "use bootfile name from the DHCP packet, don't
1596	# discover") fixes it directly, without dnsmasq's heavier --pxe-service
1597	# boot-menu machinery (which defaults discovery-control to 3, still
1598	# triggering discovery).
1599	cat >> "${conf}" <<EOF
1600interface=${vmnet}
1601dhcp-range=set:${vmnet},${client},${client},255.255.255.252,1h
1602dhcp-boot=tag:${vmnet},${bootfile}
1603dhcp-option=tag:${vmnet},tag:ipxerom,encap:43,6,8
1604dhcp-option=tag:${vmnet},tag:!ipxerom,17,"tftp://${gw}/"
1605tftp-root=${tftpdir},${vmnet}
1606EOF
1607    done < "${planfile}"
1608
1609    dnsmasq --conf-file="${conf}"
1610
1611    # dnsmasq daemonizes itself; wait for the pidfile before trusting it.
1612    tries=0
1613    while [ ! -s "${NETBOOT_STATE_DIR}/dnsmasq.pid" ] && [ ${tries} -lt 5 ]; do
1614	sleep 1
1615	tries=$((tries + 1))
1616    done
1617    [ -s "${NETBOOT_STATE_DIR}/dnsmasq.pid" ] || die "dnsmasq did not start"
1618
1619    md5 -q "${planfile}" > "${NETBOOT_STATE_DIR}/state.hash"
1620    chown "${invoker}" "${NETBOOT_STATE_DIR}" "${conf}" "${planfile}" \
1621	"${NETBOOT_STATE_DIR}/dnsmasq.pid" "${NETBOOT_STATE_DIR}/state.hash" \
1622	"${NETBOOT_STATE_DIR}"/httpd.pids "${NETBOOT_STATE_DIR}"/httpd-*.log
1623}
1624
1625# Manual cleanup: `sudo sh boot-test.sh --netboot-teardown`. Not run
1626# automatically -- leaving the setup running is what makes it "once per
1627# boot" instead of "once per run" (see netboot_network_setup).
1628netboot_teardown() {
1629    if [ -s "${NETBOOT_STATE_DIR}/dnsmasq.pid" ]; then
1630	kill "$(cat ${NETBOOT_STATE_DIR}/dnsmasq.pid)" 2>/dev/null || true
1631    fi
1632    if [ -s "${NETBOOT_STATE_DIR}/httpd.pids" ]; then
1633	while read pid; do
1634	    kill "${pid}" 2>/dev/null || true
1635	done < "${NETBOOT_STATE_DIR}/httpd.pids"
1636    fi
1637    for vmnet in $(ifconfig -g boot-test 2>/dev/null); do
1638	ifconfig "${vmnet}" destroy
1639    done
1640    rm -rf "${NETBOOT_STATE_DIR}"
1641    echo "Netboot vmnet(4)/dnsmasq setup torn down."
1642}
1643
1644assemble_all_images() {
1645    echo "=== Phase 3: Assembling disk images ==="
1646
1647    if has efi; then
1648	if [ -r "$(param efi_firmware)" ]; then
1649	    assemble_efi_gpt
1650	    if [ -f "${IMGDIR}/gptboot.esp" ]; then
1651		assemble_efi_gpt_ab
1652	    fi
1653	    if has mbr; then
1654		assemble_efi_mbr
1655	    fi
1656	elif [ -n "$(param efi_firmware)" ]; then
1657	    echo "WARNING: EFI firmware not found at $(param efi_firmware), skipping EFI tests"
1658	else
1659	    # riscv64 uses u-boot with EFI payload
1660	    assemble_efi_gpt
1661	fi
1662    fi
1663
1664    if has bios; then
1665	assemble_bios_gpt
1666	if has mbr; then
1667	    assemble_bios_mbr
1668	fi
1669    fi
1670
1671    if has bios && has efi && [ -r "$(param efi_firmware)" ]; then
1672	assemble_both_gpt
1673	if has mbr; then
1674	    assemble_both_mbr
1675	fi
1676    fi
1677
1678    if has cd; then
1679	if has prep; then
1680	    assemble_pseries_cd		# pseries SLOF CHRP CD
1681	elif has ofw; then
1682	    assemble_ofw_cd		# mac99 Apple/OF CD
1683	else
1684	    assemble_cd			# x86 El Torito
1685	fi
1686    fi
1687
1688    if has prep; then
1689	assemble_prep
1690    fi
1691
1692    # Used for mac99 emulation, though kernel issues prevent testing
1693    if has ofw; then
1694	assemble_ofw
1695    fi
1696
1697    if has linuxboot; then
1698	if [ -f "${IMGDIR}/linuxboot.esp" ]; then
1699	    assemble_linuxboot
1700	elif [ -f "${IMGDIR}/linuxboot-initrd.cpio.gz" ]; then
1701	    assemble_linuxboot_direct
1702	fi
1703    fi
1704
1705    if has netboot; then
1706	assemble_netboot
1707    fi
1708}
1709
1710# --------------------------------------------------------------------------
1711# Phase 4: Run tests in parallel
1712# --------------------------------------------------------------------------
1713
1714run_one_test() {
1715    name=$1
1716    log="${LOGDIR}/${name}.log"
1717    cmd=$(cat ${OUTDIR}/test-cmd-${name}.sh)
1718
1719    expect -c "
1720	set timeout ${TIMEOUT}
1721	log_file -noappend \"${log}\"
1722	spawn {*}${cmd}
1723	expect {
1724	    \"SUCCESS\" { exit 0 }
1725	    timeout    { exit 1 }
1726	    eof        { exit 2 }
1727	}
1728    " >/dev/null 2>&1
1729    return $?
1730}
1731
1732wait_for_slot() {
1733    # Wait until fewer than MAX_JOBS are running
1734    while true; do
1735	running=0
1736	for p in ${active_pids}; do
1737	    if kill -0 $p 2>/dev/null; then
1738		running=$((running + 1))
1739	    fi
1740	done
1741	[ ${running} -lt ${MAX_JOBS} ] && break
1742	sleep 1
1743    done
1744}
1745
1746# Launch every arch's tests together and wait once, so a run of N arches costs
1747# roughly one timeout rather than N.  Each `run_one_test &` snapshots the
1748# per-arch OUTDIR/LOGDIR/TIMEOUT that setup_arch_env just set, so backgrounded
1749# jobs keep their own arch context even as we move on to the next arch.
1750run_all_tests() {
1751    echo "=== Phase 4: Running tests ==="
1752
1753    total=0
1754    skipped=0
1755    active_pids=""
1756    results_map=$(mktemp -t boot-test-results)
1757
1758    for arch in ${ARCHES}; do
1759	setup_arch_env "${arch}"
1760	[ -s "${TESTLIST}" ] || continue
1761
1762	while read name; do
1763	    # Apply test filter if given
1764	    if [ -n "${TEST_FILTER}" ]; then
1765		echo "${name}" | grep -qE "${TEST_FILTER}" || {
1766		    skipped=$((skipped + 1))
1767		    continue
1768		}
1769	    fi
1770
1771	    total=$((total + 1))
1772
1773	    # Job throttling (global across all arches)
1774	    if [ ${MAX_JOBS} -gt 0 ]; then
1775		wait_for_slot
1776	    fi
1777
1778	    run_one_test "${name}" &
1779	    pid=$!
1780	    active_pids="${active_pids} ${pid}"
1781	    echo "${pid} ${arch} ${name}" >> ${results_map}
1782	done < ${TESTLIST}
1783    done
1784
1785    [ ${total} -gt 0 ] || die "No tests registered. Run without -B first."
1786    echo "  ${total} tests launched (${skipped} skipped by filter), waiting..."
1787    echo ""
1788
1789    # Collect results - disable errexit since wait returns the child's exit status
1790    set +e
1791    pass=0
1792    fail=0
1793    timeout_count=0
1794    while read pid arch name; do
1795	wait ${pid}
1796	rc=$?
1797	case ${rc} in
1798	    0)
1799		result="PASS"
1800		pass=$((pass + 1))
1801		;;
1802	    1)
1803		result="TIMEOUT"
1804		timeout_count=$((timeout_count + 1))
1805		;;
1806	    *)
1807		result="FAILED"
1808		fail=$((fail + 1))
1809		;;
1810	esac
1811	printf "  %-12s %-40s %s\n" "${arch}" "${name}" "${result}"
1812    done < ${results_map}
1813
1814    echo ""
1815    echo "=== Results: ${pass} passed, ${fail} failed, ${timeout_count} timed out (of ${total}) ==="
1816
1817    rm -f ${results_map}
1818    [ ${fail} -eq 0 ] && [ ${timeout_count} -eq 0 ]
1819}
1820
1821# Build the tree and images for each arch, one arch at a time.  Each arch runs
1822# in a subshell so its setup_arch_env globals stay isolated and a build failure
1823# (set -e) is caught here instead of aborting the whole run.
1824build_all() {
1825    for arch in ${ARCHES}; do
1826	echo "############################################################"
1827	echo "# ${arch}: building"
1828	echo "############################################################"
1829	if ! (
1830	    setup_arch_env "${arch}"
1831	    need_cmd "$(param qemu_bin)"
1832	    ${SKIP_BUILD} || build_tree
1833	    if ! ${SKIP_IMAGES}; then
1834		[ -d "${DESTDIR}" ] || \
1835		    die "No boot tree at ${DESTDIR}. Run without -b first."
1836		: > ${TESTLIST}		# fresh test list before (re)assembling
1837		make_base_images
1838		assemble_all_images
1839	    fi
1840	); then
1841	    echo "  ${arch}: BUILD FAILED"
1842	fi
1843    done
1844}
1845
1846# --------------------------------------------------------------------------
1847# Main
1848# --------------------------------------------------------------------------
1849
1850# --netboot-helper/--netboot-teardown are internal/manual entry points that
1851# skip the whole build+test flow below; see netboot_network_setup.
1852case "${NETBOOT_MODE}" in
1853    helper)   netboot_helper "${NETBOOT_HELPER_PLAN}"; exit $? ;;
1854    teardown) netboot_teardown; exit $? ;;
1855esac
1856
1857# Preflight: universal tools (per-arch qemu binaries are checked in build_all).
1858for prog in jq expect makefs mkimg cc; do
1859    need_cmd "${prog}"
1860done
1861
1862# gptattr sets the GPT bootme attribute for the A/B test; see
1863# assemble_efi_gpt_ab.  It is a host tool and independent of the target
1864# architecture, so build it once here rather than per-arch.
1865GPTATTR=$(mktemp -t boot-test-gptattr)
1866if ! cc -o ${GPTATTR} ${SRCTOP}/tools/boot/gptattr.c -lz 2>/dev/null; then
1867    die "Failed to build ${SRCTOP}/tools/boot/gptattr.c"
1868fi
1869
1870echo "FreeBSD boot loader test suite: ${ARCHES}"
1871echo ""
1872
1873# Accumulates netboot test plan lines across every arch (unlike ${TESTLIST},
1874# which build_all truncates per-arch) -- see register_netboot_test.
1875NETBOOT_PLAN=$(mktemp -t boot-test-netboot-plan)
1876
1877if $do_report_dirs; then
1878    for arch in ${ARCHES}; do
1879	setup_arch_env "${arch}"
1880	echo "${arch} settings:"
1881	echo "    TIMEOUT=${TIMEOUT}"
1882	echo "    ARCH_OBJDIR=${ARCH_OBJDIR}"
1883	echo "    OUTDIR=${OUTDIR}"
1884	echo "    LOGDIR=${LOGDIR}"
1885	echo "    DESTDIR=${DESTDIR}"
1886	echo "    TESTLIST=${TESTLIST}"
1887    done
1888    exit 0
1889fi
1890
1891build_all
1892netboot_network_setup
1893if run_all_tests; then
1894    rc=0
1895else
1896    rc=$?
1897fi
1898rm -f "${NETBOOT_PLAN}"
1899exit ${rc}
1900