diff --git a/mk/qemu.mk b/mk/qemu.mk index af4ec9800..f6f4cac3b 100644 --- a/mk/qemu.mk +++ b/mk/qemu.mk @@ -189,6 +189,56 @@ else QEMUFLAGS+=-machine $(QEMU_MACHINE) endif +# Share host directories with the guest through virtio-fs. +# make qemu share=/path/to/dir # one share, tag is "host" by default +# make qemu share="tag=/path/to/dir" # one share, tag is specified as "tag" +# make qemu share=/path/a share_tag=tag # one share, tag is specified as "tag" +# make qemu share="tag_1=/path/a tag_2=/path/b" # several shares: tag=dir ... +# make qemu share=redox # this build tree, tag "redox" +# make qemu share="redox=/other/path" # ordinary share, tag "redox" +# +# Each share gets its own host-side virtiofsd daemon (started and stopped by +# scripts/virtiofs-run.sh), a vhost-user-fs-pci device, and a virtio-fsd scheme +# process on Redox. In the guest, a share appears at /scheme/virtio-fsd.. +# +# WARNING: share=redox exposes the whole build tree to the guest, including +# the harddrive.img Redox is booted from, .git and .config. Writing to them +# from Redox can corrupt the build system. +share?= +share_tag?=host +share_sockdir?=$(BUILD) +QEMU_RUN= +ifneq ($(strip $(share)),) + share_norm:=$(foreach s,$(share),$(if $(filter redox,$(s)),redox=$(ROOT),$(s))) + # Normalise to a list of tag=dir entries. + ifeq ($(words $(share_norm)),1) + share_list:=$(if $(findstring =,$(share_norm)),$(share_norm),$(share_tag)=$(share_norm)) + else + share_list:=$(share_norm) + share_bare:=$(foreach s,$(share_list),$(if $(findstring =,$(s)),,$(s))) + ifneq ($(strip $(share_bare)),) + # Keep the next line free of leading tabs: make would read it as a recipe. +$(error share: every entry must be tag=dir when sharing several directories (bad: $(share_bare)); directories containing spaces are not supported) + endif + endif + share_idx:=$(shell seq 1 $(words $(share_list))) + vfs_entry=$(word $(1),$(share_list)) + vfs_tag=$(firstword $(subst =, ,$(call vfs_entry,$(1)))) + vfs_sock=$(share_sockdir)/virtiofsd-$(1).sock + + # vhost-user needs guest RAM to be shared with the virtiofsd processes, and + # the NUMA memdev must be exactly as large as -m. One backend serves all + # shares. + ifeq ($(UNAME),Linux) + QEMUFLAGS+=-object memory-backend-memfd,id=mem,size=$(QEMU_MEM)M,share=on + else + QEMUFLAGS+=-object memory-backend-file,id=mem,size=$(QEMU_MEM)M,mem-path=/dev/shm,share=on + endif + QEMUFLAGS+=-numa node,memdev=mem + QEMUFLAGS+=$(foreach i,$(share_idx),-chardev socket,id=vfs$(i),path=$(call vfs_sock,$(i)) -device vhost-user-fs-pci,queue-size=1024,chardev=vfs$(i),tag=$(call vfs_tag,$(i))) + QEMU_RUN=scripts/virtiofs-run.sh $(foreach i,$(share_idx),--share "$(call vfs_sock,$(i))" "$(call vfs_entry,$(i))") -- +endif + ifeq ($(audio),no) # No audio else ifeq ($(audio),ac97) @@ -383,9 +433,9 @@ $(BUILD)/qemu_uboot.rom: qemu: qemu-deps ifeq ($(wildcard $(MOUNT_DIR)),) - $(QEMU) $(QEMUFLAGS) + $(QEMU_RUN) $(QEMU) $(QEMUFLAGS) else @echo "\033[1;38;5;196mDisk is mounted. Unmounting the disk...\033[0m"; $(MAKE) unmount - $(QEMU) $(QEMUFLAGS) + $(QEMU_RUN) $(QEMU) $(QEMUFLAGS) endif diff --git a/podman_bootstrap.sh b/podman_bootstrap.sh index a1b2d30ed..58d077423 100755 --- a/podman_bootstrap.sh +++ b/podman_bootstrap.sh @@ -472,6 +472,110 @@ solus() fi } +############################################################################### +# virtiofsd is needed on the host by `make qemu share=` to share +# directories with Redox (scripts/virtiofs-run.sh only looks for it). +############################################################################### +virtiofsd_find() +{ + local c + for c in "${VIRTIOFSD:-}" virtiofsd /usr/libexec/virtiofsd /usr/lib/qemu/virtiofsd \ + "${XDG_CACHE_HOME:-$HOME/.cache}/virtiofsd-redox/virtiofsd"; do + [ -n "$c" ] || continue + if [ -f "$c" ] && [ -x "$c" ]; then + echo "$c" + return 0 + fi + if command -v "$c" >/dev/null 2>&1; then + command -v "$c" + return 0 + fi + done + return 1 +} + +############################################################################### +# Download a static x86_64 virtiofsd build from the upstream CI into +# ~/.cache/virtiofsd-redox, after asking for confirmation. +# Override the source with VIRTIOFSD_URL. +############################################################################### +virtiofsd_download() +{ + local url cache dest tmp answer + url="${VIRTIOFSD_URL:-https://gitlab.com/virtio-fs/virtiofsd/-/jobs/artifacts/main/raw/target/x86_64-unknown-linux-musl/release/virtiofsd?job=publish}" + cache="${XDG_CACHE_HOME:-$HOME/.cache}/virtiofsd-redox" + dest="$cache/virtiofsd" + + if [ "$(uname -m)" != "x86_64" ]; then + echo "No prebuilt virtiofsd is available for $(uname -m)." + return 1 + fi + echo "A static virtiofsd build can be downloaded from" + echo " gitlab.com/virtio-fs/virtiofsd (CI artifact of the main branch)" + echo "to $dest" + printf "Download it now? (y/N): " + read answer || answer="" + if ! echo "$answer" | grep -iq "^y"; then + return 1 + fi + + mkdir -p "$cache" + tmp="$(mktemp "$cache/virtiofsd.XXXXXX")" + if command -v curl >/dev/null 2>&1; then + curl -fL --retry 3 --retry-delay 1 -o "$tmp" "$url" || { rm -f "$tmp"; return 1; } + elif command -v wget >/dev/null 2>&1; then + wget -O "$tmp" "$url" || { rm -f "$tmp"; return 1; } + else + echo "Need curl or wget to download virtiofsd." + rm -f "$tmp" + return 1 + fi + if [ "$(head -c 4 "$tmp")" != "$(printf '\177ELF')" ]; then + echo "The download is not an ELF binary (see $url)." + rm -f "$tmp" + return 1 + fi + chmod +x "$tmp" + mv -f "$tmp" "$dest" + echo "virtiofsd installed at $dest" +} + +############################################################################### +# This function makes sure virtiofsd is available: it looks for an existing +# one, then tries the distribution package, then offers a download. +# It never fails the bootstrap, virtiofsd is only needed for `share=`. +# @params: $1 the package manager to use on Debian-based Linux +############################################################################### +virtiofsd_setup() +{ + local found + echo "Checking for virtiofsd (used by 'make qemu share=')..." + if found="$(virtiofsd_find)"; then + echo "virtiofsd found at $found" + return 0 + fi + echo "virtiofsd not found." + + if command -v pacman >/dev/null 2>&1; then + sudo pacman -S --needed virtiofsd || true + elif command -v dnf >/dev/null 2>&1; then + sudo dnf install virtiofsd || true + elif command -v apt-get >/dev/null 2>&1 && apt-cache show virtiofsd >/dev/null 2>&1; then + sudo "$1" install virtiofsd || true + fi + + if found="$(virtiofsd_find)"; then + echo "virtiofsd found at $found" + return 0 + fi + if virtiofsd_download; then + return 0 + fi + echo "Continuing without virtiofsd. 'make qemu share=' will not work until" + echo "it is installed (or set VIRTIOFSD=/path/to/virtiofsd), re-run this script" + echo "with -d to retry." +} + ###################################################################### # This function outlines the different options available for bootstrap ###################################################################### @@ -659,6 +763,11 @@ else fi fi +# Directory sharing with `make qemu share=` needs virtiofsd and a Linux host +if [ "$emulator" == "qemu" ] && [ "$(uname -s)" == "Linux" ]; then + virtiofsd_setup "$defpackman" +fi + if [ "$dependenciesonly" = false ]; then boot fi diff --git a/recipes/tests/virtio-fsd-selftest/recipe.toml b/recipes/tests/virtio-fsd-selftest/recipe.toml new file mode 100644 index 000000000..41a47c737 --- /dev/null +++ b/recipes/tests/virtio-fsd-selftest/recipe.toml @@ -0,0 +1,7 @@ +[source] +# The selftest is a member of the base workspace (base/selftests/virtio-fsd) +same_as = "../../core/base" + +[build] +template = "cargo" +cargopackages = ["virtio-fsd-selftest"] diff --git a/scripts/virtiofs-run.sh b/scripts/virtiofs-run.sh new file mode 100755 index 000000000..1d8bcebb9 --- /dev/null +++ b/scripts/virtiofs-run.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# Run a command (normally QEMU) next to one virtiofsd per shared host directory. +# +# virtiofs-run.sh --share = [--share ...] -- +# +# Used by `make qemu share=...` (see mk/qemu.mk). Each virtiofsd is started in +# the background and logs to .log. All of them are stopped +# again when the command exits, fails to start, or is interrupted. +# +# virtiofsd is taken from $VIRTIOFSD, PATH, /usr/libexec, /usr/lib/qemu or +# ~/.cache/virtiofsd-redox. podman_bootstrap.sh installs it. + +set -euo pipefail + +die() { + echo "virtiofs: $*" >&2 + exit 1 +} + +usage() { + echo "usage: $0 --share = [--share ...] -- " >&2 + exit 2 +} + +SOCKS=() +SPECS=() +while [[ "${1:-}" == --share ]]; do + [[ $# -ge 3 ]] || usage + SOCKS+=("$2") + SPECS+=("$3") + shift 3 +done +[[ ${#SOCKS[@]} -gt 0 && "${1:-}" == -- && $# -gt 1 ]] || usage +shift + +# Validate everything before starting anything, so a mistake leaves nothing running +seen_tags=" " +for i in "${!SOCKS[@]}"; do + sock="${SOCKS[i]}" spec="${SPECS[i]}" + [[ "${spec}" == *=* ]] || die "share '${spec}': expected =" + tag="${spec%%=*}" dir="${spec#*=}" + [[ "${tag}" =~ ^[A-Za-z0-9._-]+$ ]] \ + || die "tag '${tag}': only letters, digits, '.', '_' and '-' are allowed (is '${spec}' a directory with an '=' in it? use tag=dir)" + [[ ${#tag} -le 36 ]] || die "tag '${tag}': longer than 36 bytes" + [[ "${seen_tags}" != *" ${tag} "* ]] || die "tag '${tag}' is used twice" + seen_tags+="${tag} " + [[ -n "${dir}" ]] || die "share '${tag}': empty directory" + [[ "${dir}" != *[[:space:]]* ]] \ + || die "share '${tag}': directories containing whitespace are not supported ('${dir}')" + [[ "${sock}" == *.sock ]] || die "socket '${sock}' must end in .sock" + # AF_UNIX sun_path is 108 bytes including the NUL + [[ ${#sock} -lt 108 ]] \ + || die "socket path too long (${#sock} bytes): ${sock}; use a shorter share_sockdir" +done + +find_virtiofsd() { + local c + for c in "${VIRTIOFSD:-}" virtiofsd /usr/libexec/virtiofsd /usr/lib/qemu/virtiofsd \ + "${XDG_CACHE_HOME:-${HOME:-/tmp}/.cache}/virtiofsd-redox/virtiofsd"; do + [[ -n "${c}" ]] && command -v "${c}" && return 0 + done + return 1 +} + +VIRTIOFSD_BIN="$(find_virtiofsd)" \ + || die "virtiofsd not found. Install it (e.g. 'dnf install virtiofsd' or 'pacman -S virtiofsd'), run ./podman_bootstrap.sh -d, or set VIRTIOFSD=/path/to/virtiofsd" + +PIDS=() +cleanup() { + trap - EXIT INT TERM + if [[ ${#PIDS[@]} -gt 0 ]]; then + kill "${PIDS[@]}" 2>/dev/null || true + wait "${PIDS[@]}" 2>/dev/null || true + fi + rm -f "${SOCKS[@]}" "${SOCKS[@]/%/.pid}" +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +resolved_dirs=" " +for i in "${!SOCKS[@]}"; do + sock="${SOCKS[i]}" spec="${SPECS[i]}" + mkdir -p "${spec#*=}" "$(dirname "${sock}")" + dir="$(cd "${spec#*=}" && pwd)" + if [[ "${resolved_dirs}" == *" ${dir} "* ]]; then + echo "virtiofs: warning: ${dir} is shared more than once; the instances do not share caches" >&2 + fi + resolved_dirs+="${dir} " + rm -f "${sock}" "${sock}.pid" + + # The guest root maps to the invoking user, so files created in the guest + # are owned by you on the host. + # --sandbox none avoids needing root or user namespaces. + "${VIRTIOFSD_BIN}" \ + --socket-path="${sock}" \ + --shared-dir "${dir}" \ + --sandbox none \ + --log-level warn \ + --translate-uid="map:0:$(id -u):1" \ + --translate-gid="map:0:$(id -g):1" \ + "${sock%.sock}.log" 2>&1 & + PIDS+=("$!") +done + +# Wait (up to ~5 seconds per share) until every socket exists +for i in "${!SOCKS[@]}"; do + sock="${SOCKS[i]}" tag="${SPECS[i]%%=*}" log="${SOCKS[i]%.sock}.log" + for _ in $(seq 50); do + [[ -S "${sock}" ]] && break + kill -0 "${PIDS[i]}" 2>/dev/null || break + sleep 0.1 + done + if [[ ! -S "${sock}" ]]; then + echo "virtiofs: virtiofsd for '${tag}' did not create ${sock}; see ${log}" >&2 + tail -n 20 "${log}" >&2 || true + exit 1 + fi + echo "virtiofs: sharing '${tag}' = ${SPECS[i]#*=} (log: ${log})" >&2 +done + +# Run the command as a child, not with exec, so that the EXIT trap still runs +# and stops the virtiofsd processes once the command is done. +"$@"