This commit is contained in:
Softer 2026-07-14 10:46:18 +03:00 committed by GitHub
commit 08736845c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 360 additions and 0 deletions

2
.gitignore vendored
View File

@ -8,6 +8,7 @@ SAFETY_LOCK
**/**.egg*
**/**.sh
!archinstall/locales/locales_generator.sh
!test_tooling/dev_vm/dev_vm.sh
**/**.egg-info/
**/**build/
**/**src/
@ -39,6 +40,7 @@ requirements.txt
/.gitconfig
/actions-runner
/cmd_output.txt
/.dev/
node_modules/
uv.lock
test_tooling/mkosi/mkosi.output/*image*

View File

@ -170,6 +170,24 @@ replace the archinstall version with a newer one and execute the subsequent step
rare case it will not work is if the source has introduced any new dependencies that are not installed yet
- Installing the branch version with `pip install --break-system-packages .` and `archinstall`
## Developer VM (recommended for contributors)
The fastest way to iterate on archinstall is the dev VM script in `test_tooling/dev_vm/`.
It builds a minimal Arch ISO with runtime deps pre-installed, then launches QEMU with the
project source mounted read-only via 9p - edit code on the host, run `archinstall` in the
guest immediately, no rebuild needed.
```shell
./test_tooling/dev_vm/dev_vm.sh # first run builds ISO, then boots
./test_tooling/dev_vm/dev_vm.sh rebuild # force rebuild ISO
./test_tooling/dev_vm/dev_vm.sh boot # boot from installed disk (no ISO)
./test_tooling/dev_vm/dev_vm.sh --bios # legacy BIOS mode
./test_tooling/dev_vm/dev_vm.sh -h # all options
```
Requires an Arch-based host with `qemu-base` installed. See the script header for details
on 9p shares, log streaming, and env overrides.
## Without a Live ISO Image
To test this without a live ISO, the simplest approach is to use a local image and create a loop device.<br>

View File

@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""Parse pyproject.toml runtime dependencies and emit Arch package names.
Assumes the python-<name> convention, which holds for every archinstall
dependency today. If a future dep breaks the convention, fix here.
"""
import re
import sys
import tomllib
def main() -> int:
if len(sys.argv) != 2:
print(f'usage: {sys.argv[0]} <path-to-pyproject.toml>', file=sys.stderr)
return 1
with open(sys.argv[1], 'rb') as f:
data = tomllib.load(f)
for dep in data['project']['dependencies']:
name = re.split(r'[<>=!~;\[\s]', dep, maxsplit=1)[0].strip()
if name:
# PEP 503: lowercase, any run of [-_.] becomes '-'. Arch mirrors this
# naming, so normalize before prepending the python- prefix.
name = re.sub(r'[-_.]+', '-', name).lower()
print(f'python-{name}')
return 0
if __name__ == '__main__':
sys.exit(main())

307
test_tooling/dev_vm/dev_vm.sh Executable file
View File

@ -0,0 +1,307 @@
#!/bin/bash
# Dev test VM for archinstall.
# Builds a minimal dev ISO on first run (runtime deps pre-installed, 9p shares
# auto-mounted, `archinstall` wrapped to clear logs and run `python -m archinstall`),
# then launches QEMU. Source lives on the host, guest mounts it read-only - no rebuild loop.
#
# Host artifacts (all under .dev/ in repo root, git-ignored):
# .dev/iso/ generated dev ISO (mkarchiso output)
# .dev/disk.qcow2 VM disk image (qemu-img)
# .dev/ovmf-vars.fd persistent UEFI NVRAM (copied from OVMF_VARS)
# .dev/configs/ optional, user-created - shared rw to guest as /root/cfg
# .dev/logs/ auto-created - shared rw to guest as /var/log/archinstall
#
# Inside the guest after boot:
# /root/archinstall-dev project source (9p ro, host edits appear live)
# /root/cfg .dev/configs share (9p rw, mounted only if folder exists)
# /var/log/archinstall .dev/logs share (9p rw, host can tail install.log live)
# archinstall wrapper around `python -m archinstall`; clears the
# log directory on every invocation
#
# Run from the project root or from inside test_tooling/dev_vm/ - both work,
# the script resolves the project root from its own location.
#
# Usage (from project root):
# ./test_tooling/dev_vm/dev_vm.sh - build ISO if missing, fresh disk, boot
# ./test_tooling/dev_vm/dev_vm.sh rebuild | r - force rebuild ISO, fresh disk, boot
# ./test_tooling/dev_vm/dev_vm.sh keep | k - reuse disk, boot ISO
# ./test_tooling/dev_vm/dev_vm.sh boot | b - boot from installed disk (no ISO)
# ./test_tooling/dev_vm/dev_vm.sh clean | c - remove disk, NVRAM, ISO, logs
# ./test_tooling/dev_vm/dev_vm.sh -h - show this help
#
# Firmware mode (default is UEFI 64-bit):
# --bios - legacy BIOS boot (SeaBIOS, no OVMF)
#
# Flag can be combined with a command:
# ./test_tooling/dev_vm/dev_vm.sh --bios rebuild
#
# Env overrides:
# SCREEN_W, SCREEN_H - virtio-vga resolution (default 1280x720)
#
# Host FS note: 9p mapped-xattr mode needs xattr support on the filesystem
# holding the repo (ext4/btrfs work out of the box; ZFS requires xattr=sa).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
DEV_DIR="$PROJECT_DIR/.dev"
CONFIGS_DIR="$DEV_DIR/configs"
LOGS_DIR="$DEV_DIR/logs"
ISO_DIR="$DEV_DIR/iso"
DISK="$DEV_DIR/disk.qcow2"
OVMF_VARS="$DEV_DIR/ovmf-vars.fd"
DISK_SIZE="30G"
RAM="4G"
CPUS="4"
SCREEN_W="${SCREEN_W:-1280}"
SCREEN_H="${SCREEN_H:-720}"
err() { echo "ERROR: $*" >&2; exit 1; }
# Parse flags and command from arguments
FW_MODE="uefi64"
ARG="default"
for arg in "$@"; do
case "$arg" in
--bios)
FW_MODE="bios"
;;
*)
ARG="$arg"
;;
esac
done
# OVMF firmware - probed at runtime across common distro paths
OVMF_CODE=""
OVMF_VARS_ORIG=""
probe_ovmf() {
if [ -f /usr/share/edk2/x64/OVMF_CODE.4m.fd ] && [ -f /usr/share/edk2/x64/OVMF_VARS.4m.fd ]; then
OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd"
OVMF_VARS_ORIG="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
return 0
fi
echo ">>> OVMF firmware not found. Installing edk2-ovmf (sudo needed for UEFI support)..."
sudo pacman --noconfirm -S edk2-ovmf
if [ -f /usr/share/edk2/x64/OVMF_CODE.4m.fd ] && [ -f /usr/share/edk2/x64/OVMF_VARS.4m.fd ]; then
OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd"
OVMF_VARS_ORIG="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
return 0
fi
err "x64 OVMF not found at /usr/share/edk2/x64/ even after installing edk2-ovmf."
}
check_host_deps() {
local missing=() cmd
for cmd in qemu-system-x86_64 qemu-img sudo; do
command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
done
if [ ${#missing[@]} -gt 0 ]; then
err "missing host commands: ${missing[*]} (install qemu-base and sudo)"
fi
}
# ISO build needs pacman + mkarchiso, which are Arch-only. Accept Arch and
# Arch-based derivatives (Manjaro, EndeavourOS, ...) via ID / ID_LIKE.
check_arch_host() {
local id="" id_like=""
if [ -r /etc/os-release ]; then
# shellcheck disable=SC1091
. /etc/os-release
id="${ID:-}"
id_like="${ID_LIKE:-}"
fi
if [ "$id" != "arch" ] && [[ "$id_like" != *arch* ]]; then
err "ISO build requires an Arch-based host (pacman + mkarchiso). Detected ID='${id:-unknown}', ID_LIKE='${id_like:-unknown}'."
fi
}
# Parse runtime deps from pyproject.toml and map to Arch package names.
# Delegates to derive_packages.py so the Python logic lives in a real .py file.
derive_packages() {
command -v python3 >/dev/null 2>&1 || err "python3 not found on host (needed to parse pyproject.toml)"
local result
result=$(python3 "$SCRIPT_DIR/derive_packages.py" "$PROJECT_DIR/pyproject.toml") \
|| err "failed to parse $PROJECT_DIR/pyproject.toml"
[ -n "$result" ] || err "pyproject.toml has no [project.dependencies]"
printf '%s\n' "$result"
}
find_iso() {
ls -t "$ISO_DIR"/archlinux-*-x86_64.iso 2>/dev/null | head -n1
}
build_iso() {
check_arch_host
if ! command -v mkarchiso >/dev/null 2>&1; then
echo ">>> mkarchiso not found on host; archiso will be installed via sudo pacman."
fi
local runtime_deps
runtime_deps=$(derive_packages)
echo ">>> Runtime deps derived from pyproject.toml:"
echo "$runtime_deps" | sed 's/^/ /'
echo ">>> Building dev ISO (sudo needed for pacman/mkarchiso)..."
sudo env OUT_DIR="$ISO_DIR" HOST_USER="$(id -un)" RUNTIME_DEPS="$runtime_deps" bash <<'SUDO_SCRIPT'
set -e
BUILD_DIR=/tmp/archlive-dev
# Runtime deps - python plus the list derived from pyproject.toml on the host.
# Source comes via 9p, no wheel build needed.
packages=(python git)
while IFS= read -r p; do
[ -n "$p" ] && packages+=("$p")
done <<< "$RUNTIME_DEPS"
rm -rf "$BUILD_DIR"
pacman --noconfirm --needed -S archiso
cp -r /usr/share/archiso/configs/releng "$BUILD_DIR"
# Drop preinstalled archinstall - we run from 9p-mounted source.
# Anchored pattern so related packages (archinstall-*, python-archinstall-*)
# would not be accidentally removed if releng ever grows them.
sed -i '/^archinstall$/d' "$BUILD_DIR/packages.x86_64"
for p in "${packages[@]}"; do
echo "$p" >> "$BUILD_DIR/packages.x86_64"
done
# Trust the 9p-mounted source for git: host UIDs differ from the guest's,
# which would otherwise trip git's safe.directory dubious-ownership check.
mkdir -p "$BUILD_DIR/airootfs/etc"
cat > "$BUILD_DIR/airootfs/etc/gitconfig" <<'GIT'
[safe]
directory = /root/archinstall-dev
GIT
# Auto-mount project, define archinstall wrapper, print info on login
mkdir -p "$BUILD_DIR/airootfs/root"
cat > "$BUILD_DIR/airootfs/root/.zprofile" <<'ZP'
mkdir -p /root/archinstall-dev /root/cfg /var/log/archinstall
if ! mount -t 9p -o trans=virtio,ro dev /root/archinstall-dev 2>/dev/null; then
echo "ERROR: failed to mount 9p 'dev' share. The archinstall wrapper will not work."
echo "Check host qemu virtfs support and that the guest kernel has the 9p module."
fi
mount -t 9p -o trans=virtio cfg /root/cfg 2>/dev/null || true
mount -t 9p -o trans=virtio logs /var/log/archinstall 2>/dev/null || true
cd /root/archinstall-dev
export PYTHONDONTWRITEBYTECODE=1
# Wrapper instead of a plain alias so each invocation starts with a clean log
# directory. The directory is a 9p mount from the host, so the contents are
# wiped (find -delete) instead of removing the mountpoint itself.
archinstall() {
find /var/log/archinstall -mindepth 1 -delete 2>/dev/null
python -m archinstall "$@"
}
cat <<MSG
=== archinstall dev environment ===
Source: /root/archinstall-dev (9p, read-only, live host edits)
Configs: /root/cfg (9p, optional, if .dev/configs on host)
Logs: /var/log/archinstall (9p, host sees install.log live in .dev/logs/)
Run: archinstall (wraps 'python -m archinstall'; clears /var/log/archinstall first)
MSG
ZP
mkdir -p "$OUT_DIR"
cd "$BUILD_DIR"
mkarchiso -v -w work/ -o "$OUT_DIR" ./
chown -R "$HOST_USER:" "$OUT_DIR"
SUDO_SCRIPT
echo ">>> ISO built."
}
case "$ARG" in
-h|--help|help)
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
clean|c)
rm -fv "$DISK" "$OVMF_VARS"
rm -rf "$ISO_DIR" "$LOGS_DIR"
exit 0
;;
boot|b)
check_host_deps
[ "$FW_MODE" != "bios" ] && probe_ovmf
[ -f "$DISK" ] || err "Disk missing, run without args first"
BOOT_ORDER="c"
ATTACH_ISO=false
;;
rebuild|r)
check_host_deps
[ "$FW_MODE" != "bios" ] && probe_ovmf
rm -rf "$ISO_DIR"
build_iso
rm -f "$DISK"
qemu-img create -f qcow2 "$DISK" "$DISK_SIZE"
BOOT_ORDER="d"
ATTACH_ISO=true
;;
keep|k)
check_host_deps
[ "$FW_MODE" != "bios" ] && probe_ovmf
[ -f "$DISK" ] || err "Disk missing, run without args first"
[ -n "$(find_iso)" ] || build_iso
BOOT_ORDER="d"
ATTACH_ISO=true
;;
default)
check_host_deps
[ "$FW_MODE" != "bios" ] && probe_ovmf
[ -n "$(find_iso)" ] || build_iso
rm -f "$DISK"
qemu-img create -f qcow2 "$DISK" "$DISK_SIZE"
BOOT_ORDER="d"
ATTACH_ISO=true
;;
*)
err "Unknown argument: $ARG (try -h)"
;;
esac
if [ "$FW_MODE" != "bios" ]; then
[ -f "$OVMF_VARS" ] || cp "$OVMF_VARS_ORIG" "$OVMF_VARS"
fi
QEMU_ARGS=(
-machine q35
-cpu host
-enable-kvm
-m "$RAM"
-smp "$CPUS"
-drive "file=$DISK,format=qcow2,if=virtio"
-device virtio-net-pci,netdev=net0
-netdev user,id=net0
-device "virtio-vga,xres=$SCREEN_W,yres=$SCREEN_H"
-display gtk,zoom-to-fit=off
-monitor stdio
-virtfs "local,path=$PROJECT_DIR,mount_tag=dev,security_model=mapped-xattr,readonly=on"
-boot "order=$BOOT_ORDER"
)
if [ "$FW_MODE" != "bios" ]; then
QEMU_ARGS+=(
-drive "if=pflash,format=raw,readonly=on,file=$OVMF_CODE"
-drive "if=pflash,format=raw,file=$OVMF_VARS"
)
fi
# Optional second 9p share for test configs, only if host folder exists
if [ -d "$CONFIGS_DIR" ]; then
QEMU_ARGS+=(-virtfs "local,path=$CONFIGS_DIR,mount_tag=cfg,security_model=mapped-xattr")
fi
# Logs share: always mounted, host folder auto-created so install.log is
# visible from the host (.dev/logs/install.log) without re-entering the VM.
mkdir -p "$LOGS_DIR"
QEMU_ARGS+=(-virtfs "local,path=$LOGS_DIR,mount_tag=logs,security_model=mapped-xattr")
if [ "$ATTACH_ISO" = "true" ]; then
ISO="$(find_iso)"
[ -n "$ISO" ] && [ -f "$ISO" ] || err "No dev ISO after build step"
QEMU_ARGS+=(-cdrom "$ISO")
fi
exec qemu-system-x86_64 "${QEMU_ARGS[@]}"