Merge 384045c2fd into 53158a8aad
This commit is contained in:
commit
08736845c3
|
|
@ -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*
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -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[@]}"
|
||||
Loading…
Reference in New Issue