diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8a768a6 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,58 @@ +name: Build and test + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: test-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + images: + runs-on: ubuntu-26.04 + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + arch: [aarch64, x86_64] + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + - name: Install test dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential flex bison bc perl pkg-config libelf-dev libssl-dev \ + curl ca-certificates tar xz-utils bzip2 gzip cpio python3 \ + qemu-system-x86 qemu-system-arm virtiofsd openssh-client sshpass + - name: Enable vsock transport + run: | + sudo modprobe vhost_vsock + sudo udevadm settle + sudo chmod a+rw /dev/vhost-vsock + python3 -c 'import os; os.close(os.open("/dev/vhost-vsock", os.O_RDWR))' + - name: Restore toolchain and kernel build + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ${{ matrix.arch }}-linux-musl-*/ + build-${{ matrix.arch }}/linux-kernel/ + key: kernel-${{ runner.os }}-26.04-${{ matrix.arch }}-${{ hashFiles('puipui-linux-tool', 'kconfig/*.config', 'tests/toolchains.sha256') }} + - name: Build and test image + run: bash tests/run.sh "${{ matrix.arch }}" + - name: Upload diagnostics + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: test-logs-${{ matrix.arch }} + path: build-tests/ + - name: Upload tested images + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: puipui-images-${{ matrix.arch }} + path: puipui_linux_*.tar.gz diff --git a/CLAUDE.md b/CLAUDE.md index aa9f289..8b75ce4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ This document provides comprehensive information about the PUI PUI Linux codebas ### Current Version - Tool Version: 1.0.3 -- Kernel Version: 6.11.5 +- Kernel Version: 7.2.9 - BusyBox Version: 1.36.1 - Dropbear Version: 2024.86 - Socat Version: 1.8.0.1 @@ -153,7 +153,7 @@ These are gitignored along with downloaded source tarballs. **Kernel Configuration Management:** - Configs stored as defconfig format in `kconfig/{arch}.config` - Use `./puipui-linux-tool -u` to update configs -- Process: `listnewconfig` → `oldconfig` → `savedefconfig` +- Process: `olddefconfig` → `savedefconfig` (both target toolchains; failures stop the command) - Always use `olddefconfig` for non-interactive updates ### 2. Initramfs & Init System @@ -387,7 +387,7 @@ git push -u origin ### Update to New Kernel Version -1. Edit `puipui-linux-tool`: change `kernver=6.11.5` to new version +1. Edit `puipui-linux-tool`: change `kernver=7.2.9` to new version 2. Run `./puipui-linux-tool` - it will download new kernel 3. May need to update configs: `./puipui-linux-tool -u` 4. Test both architectures diff --git a/README.md b/README.md index 0ea85a4..57efb44 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ Since file size is very small in totally, I can do testing on the any CI with do ## Kernel +The kernel is pinned to Linux **7.2.9** (stable). Both architectures use +minimal configs in `kconfig/`; new kernel options take their upstream defaults. + What's supported: - Internet @@ -35,16 +38,28 @@ You can use it with other linux distributions. ### Build all +Build on Linux with GCC/binutils, make, flex, bison, bc, Perl, pkg-config, +libelf development headers, curl, tar, xz, bzip2, gzip and cpio installed. +The script downloads the target musl toolchains separately from the host tools. + ``` $ ./puipui-linux-tool ``` ### Update Kernel config +Set `kernver` in `puipui-linux-tool`, then run: + ``` $ ./puipui-linux-tool -u ``` +This downloads the kernel source and missing toolchains, runs `olddefconfig` +and `savedefconfig` for both architectures, and saves the minimal configs. +An existing `build-{arch}/linux-kernel/.config` is preserved as the input; +otherwise the checked-in config is used. Errors stop the command. +Review the config diff, then build and boot both architectures. + ## Hack This tool is used musl cross compiler. On arm64 environment the tool will be downloaded those pre build toolchains from https://github.com/Code-Hex/musl-cross-make-arm64 diff --git a/kconfig/aarch64.config b/kconfig/aarch64.config index 5432167..4a711da 100644 --- a/kconfig/aarch64.config +++ b/kconfig/aarch64.config @@ -46,7 +46,6 @@ CONFIG_EXPERT=y # CONFIG_SOCIONEXT_SYNQUACER_PREITS is not set # CONFIG_ARM64_TAGGED_ADDR_ABI is not set # CONFIG_ARM64_HW_AFDBM is not set -# CONFIG_ARM64_PAN is not set # CONFIG_ARM64_RAS_EXTN is not set # CONFIG_ARM64_CNP is not set # CONFIG_ARM64_PTR_AUTH is not set @@ -65,7 +64,6 @@ CONFIG_COMPAT_32BIT_TIME=y # CONFIG_SLAB_MERGE_DEFAULT is not set # CONFIG_COMPAT_BRK is not set # CONFIG_COMPACTION is not set -# CONFIG_MIGRATION is not set CONFIG_NET=y CONFIG_PACKET=y CONFIG_UNIX=y @@ -98,7 +96,6 @@ CONFIG_VIRTIO_NET=y # CONFIG_NET_VENDOR_ADAPTEC is not set # CONFIG_NET_VENDOR_AGERE is not set # CONFIG_NET_VENDOR_ALACRITECH is not set -# CONFIG_NET_VENDOR_ALTEON is not set # CONFIG_NET_VENDOR_AMAZON is not set # CONFIG_NET_VENDOR_AMD is not set # CONFIG_NET_VENDOR_AQUANTIA is not set @@ -128,11 +125,9 @@ CONFIG_VIRTIO_NET=y # CONFIG_NET_VENDOR_MYRI is not set # CONFIG_NET_VENDOR_NI is not set # CONFIG_NET_VENDOR_NATSEMI is not set -# CONFIG_NET_VENDOR_NETERION is not set # CONFIG_NET_VENDOR_NETRONOME is not set # CONFIG_NET_VENDOR_NVIDIA is not set # CONFIG_NET_VENDOR_OKI is not set -# CONFIG_NET_VENDOR_PACKET_ENGINES is not set # CONFIG_NET_VENDOR_PENSANDO is not set # CONFIG_NET_VENDOR_QLOGIC is not set # CONFIG_NET_VENDOR_BROCADE is not set diff --git a/kconfig/x86_64.config b/kconfig/x86_64.config index c4f7261..9226c25 100644 --- a/kconfig/x86_64.config +++ b/kconfig/x86_64.config @@ -87,16 +87,15 @@ CONFIG_INPUT_EVDEV=y # CONFIG_LEGACY_TIOCSTI is not set # CONFIG_LDISC_AUTOLOAD is not set CONFIG_SERIAL_8250=y -# CONFIG_SERIAL_8250_DEPRECATED_OPTIONS is not set # CONFIG_SERIAL_8250_PNP is not set CONFIG_SERIAL_8250_CONSOLE=y # CONFIG_SERIAL_8250_PCI is not set # CONFIG_SERIAL_8250_EXAR is not set CONFIG_SERIAL_8250_NR_UARTS=16 CONFIG_SERIAL_8250_EXTENDED=y -CONFIG_SERIAL_8250_MANY_PORTS=y CONFIG_SERIAL_8250_SHARE_IRQ=y CONFIG_SERIAL_8250_RSA=y +CONFIG_SERIAL_8250_MANY_PORTS=y # CONFIG_SERIAL_8250_LPSS is not set # CONFIG_SERIAL_8250_MID is not set # CONFIG_SERIAL_8250_PERICOM is not set diff --git a/puipui-linux-tool b/puipui-linux-tool index 998feed..8600f69 100755 --- a/puipui-linux-tool +++ b/puipui-linux-tool @@ -6,6 +6,7 @@ # This script builds puipui linux kernel and it's initramfs. # # Options: +# -a Build only the specified architecture (aarch64 or x86_64). # -u Only updates linux kernel config files. it does not build kernel or initramfs. #---help--- @@ -15,7 +16,7 @@ version=1.0.3 srcdir="$PWD" target_arch="aarch64 x86_64" -kernver=6.18.3 +kernver=7.2.9 busyboxver=1.36.1 dropbearver=2024.86 socatver=1.8.0.1 @@ -34,7 +35,7 @@ usage() { fetch() { local url="$1" local archive=${url##*/} - (cd $srcdir && curl -OL $url) + (cd "$srcdir" && curl -fOL "$url") tar -xvf "$srcdir"/"$archive" -C "$srcdir" } @@ -66,7 +67,7 @@ _artifact() { _cross_compile_target() { local arch="$1" - echo $arch-linux-musl- + echo "$srcdir/$(_musl_path "$arch")/bin/$arch-linux-musl-" } _musl_path() { @@ -85,14 +86,21 @@ setup_musl_path() { local musl_abs_path="$srcdir/$musl_path" local musl_prefix="$arch-linux-musl-" local machine_arch="$(uname -m)" - if [ "aarch64" == $machine_arch ]; then - fetch "https://github.com/Code-Hex/musl-cross-make-arm64/releases/download/v0.0.4/${musl_path}.tar.gz" - tar xvf "$musl_abs_path.tar.gz" - else - fetch "https://musl.cc/${musl_path}.tgz" - tar xvf "$musl_abs_path.tgz" - ln -s $musl_abs_path/bin/{strip,"${musl_prefix}strip"} + if ! [ -d "$musl_abs_path" ]; then + msg "Download $musl_path" + if [ "aarch64" == $machine_arch ]; then + fetch "https://github.com/Code-Hex/musl-cross-make-arm64/releases/download/v0.0.4/${musl_path}.tar.gz" + else + fetch "https://musl.cc/${musl_path}.tgz" + fi fi + # Native toolchains ship binutils without the target prefix. + local tool + for tool in ar as ld nm objcopy objdump ranlib readelf strip; do + if ! [ -e "$musl_abs_path/bin/${musl_prefix}${tool}" ]; then + ln -s "$tool" "$musl_abs_path/bin/${musl_prefix}${tool}" + fi + done } _prepareconfig() { @@ -101,7 +109,6 @@ _prepareconfig() { local _builddir=$(_builddir $_arch linux-kernel) mkdir -p "$_builddir" - local PATH="$srcdir/$(_musl_path $_arch)/bin:$PATH" cp "$srcdir"/$_config "$_builddir"/.config msg "Configuring $name kernel ($_arch)" make -C "$srcdir"/linux-$kernver \ @@ -128,11 +135,10 @@ build_kern() { else echo "Unknown TARGET $_arch"; exit 1; fi - local PATH="$srcdir/$(_musl_path $_arch)/bin:$PATH" cd $(_builddir $_arch linux-kernel) make CROSS_COMPILE=$(_cross_compile_target $_arch) \ ARCH="$(_kernelarch $_arch)" \ - AWK="${AWK:-mawk}" \ + AWK="${AWK:-awk}" \ KBUILD_BUILD_VERSION="$version-PUI PUI!" \ KBUILD_BUILD_HOST="molbuilder" \ KBUILD_BUILD_USER="codehex" \ @@ -182,7 +188,6 @@ build_initramfs() { cp "$srcdir"/busybox_config/config "$_builddir"/.config - local PATH="$srcdir/$(_musl_path $_arch)/bin:$PATH" make -C "$srcdir"/busybox-$busyboxver \ CROSS_COMPILE=$(_cross_compile_target $_arch) \ O="$_builddir" \ @@ -220,7 +225,6 @@ install_dropbear() { local _arch="$1" local _fsdir="$2" - local PATH="$srcdir/$(_musl_path $_arch)/bin:$PATH" local _target=$(_cross_compile_target $_arch) local CC="${_target}cc" local AR="${_target}ar" @@ -230,7 +234,9 @@ install_dropbear() { local _dropbear_src="$srcdir"/"dropbear-${dropbearver}" mkdir -p "$_builddir" - (cd $_dropbear_src && ./configure --build=$_arch -prefix=$_fsdir --disable-zlib --enable-static --disable-loginfunc --disable-wtmp) + (cd $_dropbear_src && CC="$CC" AR="$AR" RANLIB="$RANLIB" STRIP="$STRIP" \ + ./configure --build="$(gcc -dumpmachine)" --host="$_arch-linux-musl" \ + --prefix="$_fsdir" --disable-zlib --enable-static --disable-loginfunc --disable-wtmp) cd $_builddir make -C $_dropbear_src \ @@ -242,7 +248,6 @@ install_socat() { local _arch="$1" local _fsdir="$2" - local PATH="$srcdir/$(_musl_path $_arch)/bin:$PATH" local _target=$(_cross_compile_target $_arch) local CC="${_target}cc" local AR="${_target}ar" @@ -257,8 +262,10 @@ install_socat() { # https://git.alpinelinux.org/aports/commit/main/socat/APKBUILD?id=5edc9195355ced3db991c1a7cda5648d52019b11 ( cd $_socat_src && \ + CC="$CC" AR="$AR" RANLIB="$RANLIB" STRIP="$STRIP" \ CFLAGS="-static -Os -s" ./configure \ - --build=$_arch \ + --build="$(gcc -dumpmachine)" \ + --host="$_arch-linux-musl" \ --prefix=$_fsdir \ --disable-man \ --disable-openssl \ @@ -291,16 +298,15 @@ updateconfigs() { local _config=$(_kconfig $_arch.config) local _builddir=$(_builddir $_arch linux-kernel) mkdir -p "$_builddir" - local actions="listnewconfig oldconfig" if ! [ -f "$_builddir"/.config ]; then cp "$srcdir"/$_config "$_builddir"/.config - actions="olddefconfig" env | grep ^CONFIG_ >> "$_builddir"/.config || true fi make -j1 -C "$srcdir"/linux-$kernver \ + CROSS_COMPILE=$(_cross_compile_target $_arch) \ O="$_builddir" \ ARCH="$(_kernelarch $_arch)" \ - $actions savedefconfig + olddefconfig savedefconfig cp "$_builddir"/defconfig "$srcdir"/$_config done @@ -308,17 +314,33 @@ updateconfigs() { -while getopts ':uhv' OPTION; do +update_only=false +while getopts ':a:uhv' OPTION; do case "$OPTION" in - u) updateconfigs || true; exit 0;; + a) + case "$OPTARG" in + aarch64|x86_64) target_arch="$OPTARG";; + *) echo "Unsupported architecture: $OPTARG" >&2; exit 1;; + esac;; + u) update_only=true;; h) usage; exit 0;; v) echo "puipui-linux-tool $version"; exit 0;; + *) usage >&2; exit 1;; esac done if ! [ -d linux-"$kernver" ]; then msg "Download linux kernel $kernver" - fetch "https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${kernver}.tar.xz" + fetch "https://cdn.kernel.org/pub/linux/kernel/v${kernver%%.*}.x/linux-${kernver}.tar.xz" +fi + +for _arch in $target_arch; do + setup_musl_path "$_arch" +done + +if $update_only; then + updateconfigs + exit 0 fi if ! [ -d busybox-"$busyboxver" ]; then @@ -336,16 +358,6 @@ if ! [ -d socat-"$socatver" ]; then fetch "http://www.dest-unreach.org/socat/download/socat-${socatver}.tar.gz" fi -if ! [ -d $(_musl_path "aarch64") ]; then - msg "Download aarch64-linux-musl" - setup_musl_path "aarch64" -fi - -if ! [ -d $(_musl_path "x86_64") ]; then - msg "Download x86_64-linux-musl" - setup_musl_path "x86_64" -fi - build_kern download_curl build_initramfs diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..0f9d863 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,56 @@ +# Build and VM tests + +Run directly on an x86_64 Linux host. CI uses Ubuntu 26.04; its virtiofsd +supports read-only exports and UID/GID translation. Both guests use QEMU TCG, +so KVM and nested virtualization are not required. + +On Ubuntu 26.04, install the dependencies and run from the repository root: + +```sh +sudo apt-get update +sudo apt-get install -y --no-install-recommends \ + build-essential flex bison bc perl pkg-config libelf-dev libssl-dev \ + curl ca-certificates tar xz-utils bzip2 gzip cpio python3 \ + qemu-system-x86 qemu-system-arm virtiofsd openssh-client sshpass +sudo modprobe vhost_vsock +sudo chmod a+rw /dev/vhost-vsock +bash tests/run.sh +``` + +To build and test only one architecture, use `bash tests/run.sh aarch64` or +`bash tests/run.sh x86_64`. The build tool also accepts `-a aarch64` or +`-a x86_64`; without it, both architectures are built as before. + +The suite obtains missing x86_64-hosted toolchains from the pinned +`musl-cc/musl.cc` GitHub mirror because musl.cc blocks Actions traffic. +`tests/toolchains.sha256` pins hashes verified against the original musl.cc +archives; extraction happens only after verification. Existing local toolchains +are reused. + +Actions runs each architecture on a separate runner in parallel and caches +its toolchain and kernel build directory. The cache key includes the build +script, kernel configs, and toolchain checksums. A cache hit still runs the +build and all checks; it only avoids recompiling unchanged kernel objects. +Userspace and release archives are rebuilt on every run. + +The suite checks config stability and config-update failure handling, +and boots the resulting release archives with 256 MiB of RAM. VM checks cover +the kernel version, DHCP, password SSH login, virtiofs read/write and read-only +exports, console and RNG devices, the SSH banner over default/custom vsock +ports, and guest poweroff. + +Logs are written to `build-tests/`, including QEMU and guest console output. +Actions uploads these logs even on failure and uploads the archives only after +all tests pass. Source downloads and build directories remain available for +local reruns. For an existing build, rerun one VM test with: + +```sh +python3 tests/smoke.py aarch64 puipui_linux_v1.0.3_aarch64.tar.gz 7.2.9 +``` + +The standalone VM test needs Python 3, QEMU, virtiofsd, OpenSSH and sshpass, +and access to `/dev/vhost-vsock`. Ports 12222/12223 and guest vsock CIDs 42/43 +must be unused. Run only one suite per host at a time. + +These tests do not exercise Apple Virtualization Framework, host-requested +shutdown, or an aarch64 build host. diff --git a/tests/config_failure.py b/tests/config_failure.py new file mode 100644 index 0000000..c12523a --- /dev/null +++ b/tests/config_failure.py @@ -0,0 +1,27 @@ +"""A failed config update must report failure and preserve the source configs.""" +import pathlib +import platform +import shutil +import subprocess +import sys +import tempfile + +root = pathlib.Path(__file__).resolve().parent.parent +with tempfile.TemporaryDirectory(prefix="puipui-config-") as directory: + work = pathlib.Path(directory) + (work / f"linux-{sys.argv[1]}").symlink_to(root / f"linux-{sys.argv[1]}") + for arch in ("aarch64", "x86_64"): + suffix = "native" if arch == platform.machine() else "cross" + (work / f"{arch}-linux-musl-{suffix}" / "bin").mkdir(parents=True) + shutil.copytree(root / "kconfig", work / "kconfig") + result = subprocess.run( + [str(root / "puipui-linux-tool"), "-u"], cwd=work, + stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=60, + ) + (root / "build-tests/config-failure.log").write_text(result.stdout + result.stderr) + if result.returncode == 0 or "not found" not in result.stderr: + raise RuntimeError("Missing compiler did not produce the expected failure") + for config in (work / "kconfig").iterdir(): + if config.read_bytes() != (root / "kconfig" / config.name).read_bytes(): + raise RuntimeError(f"Failed update changed {config.name}") +print("Config failure propagation and preservation: PASS") diff --git a/tests/run.sh b/tests/run.sh new file mode 100644 index 0000000..02acb5e --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -euo pipefail +cd "$(dirname "$0")/.." +mkdir -p build-tests +architectures="${1:-aarch64 x86_64}" +args=() +if [ "$#" -gt 0 ]; then + case "$1" in + aarch64|x86_64) args=(-a "$1");; + *) echo "Unsupported architecture: $1" >&2; exit 1;; + esac +fi + +# musl.cc blocks Actions; these mirror files match the original archives. +while read -r checksum archive; do + arch=${archive%%-linux-*} + if [[ " $architectures " != *" $arch "* ]]; then + continue + fi + if [ ! -d "${archive%.tgz}" ]; then + ( + staging=$(mktemp -d) + trap 'rm -rf "$staging"' EXIT + curl -fL --retry 3 --connect-timeout 30 \ + "https://github.com/musl-cc/musl.cc/releases/download/v0.0.1/$archive" \ + -o "$staging/$archive" + printf '%s %s\n' "$checksum" "$staging/$archive" | sha256sum --check + tar -xzf "$staging/$archive" -C "$staging" + mv "$staging/${archive%.tgz}" . + ) + fi +done < tests/toolchains.sha256 + +# Keep the build's non-interactive input and diagnostics identical in CI and locally. +bash -n puipui-linux-tool +echo "Checking fresh and repeated config updates" +sha256sum kconfig/*.config >build-tests/config.sha256 +./puipui-linux-tool "${args[@]}" -u build-tests/config.log 2>&1 +sha256sum --check build-tests/config.sha256 +./puipui-linux-tool "${args[@]}" -u >build-tests/config.log 2>&1 +sha256sum --check build-tests/config.sha256 + +echo "Building $architectures (see build-tests/build.log)" +./puipui-linux-tool "${args[@]}" build-tests/build.log 2>&1 +version=$(sed -n 's/^version=//p' puipui-linux-tool) +kernel=$(sed -n 's/^kernver=//p' puipui-linux-tool) +python3 tests/config_failure.py "$kernel" +for arch in $architectures; do + python3 tests/smoke.py "$arch" "puipui_linux_v${version}_${arch}.tar.gz" "$kernel" +done diff --git a/tests/smoke.py b/tests/smoke.py new file mode 100644 index 0000000..03fecf1 --- /dev/null +++ b/tests/smoke.py @@ -0,0 +1,157 @@ +"""Boot the packaged images, exercise guest interfaces, and retain failure logs.""" +import argparse +import pathlib +import os +import shutil +import socket +import subprocess +import tarfile +import tempfile +import time + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("arch", choices=("aarch64", "x86_64")) + parser.add_argument("archive", type=pathlib.Path) + parser.add_argument("kernel_version") + args = parser.parse_args() + root = pathlib.Path(__file__).resolve().parent.parent + logs_dir = root / "build-tests" / args.arch + logs_dir.mkdir(parents=True, exist_ok=True) + kernel, machine, cpu, port, cid, vsock_port = { + "aarch64": ("Image.gz", "virt", "cortex-a72", 12223, 43, 2345), + "x86_64": ("bzImage", "q35", "max", 12222, 42, 2222), + }[args.arch] + processes = [] + logs = [] + + def start(command, name): + log = open(logs_dir / name, "w") + logs.append(log) + process = subprocess.Popen(command, stdin=subprocess.DEVNULL, stdout=log, + stderr=subprocess.STDOUT) + processes.append(process) + return process + + with tempfile.TemporaryDirectory(prefix="puipui-smoke-") as directory: + work = pathlib.Path(directory) + + def ssh(command): + return subprocess.run( + ["sshpass", "-p", "passwd", "ssh", "-o", "StrictHostKeyChecking=no", + "-o", f"UserKnownHostsFile={work}/known_hosts", "-o", "ConnectTimeout=3", + "-p", str(port), "root@127.0.0.1", command], + capture_output=True, text=True, timeout=15, + ) + + try: + # Copy only the expected regular files; reject incompatible archive layouts. + with tarfile.open(args.archive, "r:gz") as archive: + files = [member for member in archive.getmembers() if not member.isdir()] + if sorted(member.name.removeprefix("./") for member in files) != sorted([kernel, "initramfs.cpio.gz"]): + raise RuntimeError("Unexpected archive contents") + for member in files: + if not member.isfile(): + raise RuntimeError("Archive contains a non-regular file") + with archive.extractfile(member) as source: + (work / member.name.removeprefix("./")).write_bytes(source.read()) + shared = work / "shared" + shared.mkdir() + (shared / "host.txt").write_text("from-host\n") + virtiofsd = shutil.which("virtiofsd") or "/usr/libexec/virtiofsd" + for tag in ("rw", "ro"): + command = [virtiofsd, "--shared-dir", str(shared), "--socket-path", + str(work / f"{tag}.sock"), "--sandbox", "none", + "--translate-uid", f"map:0:{os.getuid()}:1", + "--translate-gid", f"map:0:{os.getgid()}:1"] + if tag == "ro": + command.append("--readonly") + daemon = start(command, f"virtiofs-{tag}.log") + deadline = time.monotonic() + 10 + while not (work / f"{tag}.sock").exists(): + if daemon.poll() is not None or time.monotonic() >= deadline: + raise RuntimeError(f"virtiofsd failed; see {logs_dir}") + time.sleep(.1) + command = [f"qemu-system-{args.arch}", "-machine", machine, "-accel", "tcg", + "-cpu", cpu, "-m", "256", "-smp", "1", "-display", "none", + "-monitor", "none", "-serial", "none", "-kernel", str(work / kernel), + "-initrd", str(work / "initramfs.cpio.gz"), "-append", + "console=hvc0" + (f" vsock_port={vsock_port}" if args.arch == "aarch64" else ""), + "-object", "memory-backend-memfd,id=mem,size=256M,share=on", + "-numa", "node,memdev=mem", "-device", "virtio-serial-pci", + "-chardev", f"file,id=console,path={logs_dir}/console.log", + "-device", "virtconsole,chardev=console", "-netdev", + f"user,id=net,hostfwd=tcp:127.0.0.1:{port}-:22", + "-device", "virtio-net-pci,netdev=net,romfile=", + "-device", "virtio-rng-pci", "-device", "virtio-balloon-pci", + "-device", f"vhost-vsock-pci,guest-cid={cid}"] + for tag in ("rw", "ro"): + command += ["-chardev", f"socket,id={tag},path={work}/{tag}.sock", + "-device", f"vhost-user-fs-pci,chardev={tag},tag={tag}"] + vm = start(command, "qemu.log") + deadline = time.monotonic() + 180 + while time.monotonic() < deadline: + if vm.poll() is not None: + raise RuntimeError(f"QEMU exited early; see {logs_dir}") + try: + result = ssh("uname -r") + except subprocess.TimeoutExpired: + continue + (logs_dir / "ssh.log").write_text(result.stdout + result.stderr) + if result.returncode == 0: + if result.stdout.strip() != args.kernel_version: + raise RuntimeError(f"Unexpected kernel: {result.stdout}") + break + time.sleep(1) + else: + raise RuntimeError(f"Guest SSH did not become ready; see {logs_dir}") + result = ssh('''set -eu +ip addr show eth0 +mkdir -p /mnt/rw /mnt/ro +mount -t virtiofs rw /mnt/rw +mount -t virtiofs ro /mnt/ro +test "$(cat /mnt/rw/host.txt)" = from-host +test "$(cat /mnt/ro/host.txt)" = from-host +echo from-guest > /mnt/rw/guest.txt +if touch /mnt/ro/must-not-exist 2>/dev/null; then exit 1; fi +test -c /dev/hvc0 +test -c /dev/hwrng +''') + (logs_dir / "guest.log").write_text(result.stdout + result.stderr) + if result.returncode or (shared / "guest.txt").read_text() != "from-guest\n": + raise RuntimeError(f"Guest interface checks failed; see {logs_dir}") + console = (logs_dir / "console.log").read_text() + if "lease of " not in console or " obtained from " not in console: + raise RuntimeError("Guest did not obtain a DHCP lease") + with socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM) as connection: + connection.settimeout(10) + connection.connect((cid, vsock_port)) + banner = b"" + while b"\n" not in banner and len(banner) < 256: + chunk = connection.recv(256) + if not chunk: + break + banner += chunk + if not banner.startswith(b"SSH-2.0-dropbear"): + raise RuntimeError(f"Unexpected vsock response: {banner!r}") + ssh("poweroff") + if vm.wait(timeout=30) != 0: + raise RuntimeError("Guest shutdown failed") + print(f"{args.arch}: {args.kernel_version}, DHCP, SSH, virtiofs RW/RO, " + f"vsock {vsock_port}, poweroff: PASS", flush=True) + finally: + for process in reversed(processes): + if process.poll() is None: + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + for log in logs: + log.close() + + +if __name__ == "__main__": + main() diff --git a/tests/toolchains.sha256 b/tests/toolchains.sha256 new file mode 100644 index 0000000..69604b8 --- /dev/null +++ b/tests/toolchains.sha256 @@ -0,0 +1,2 @@ +c909817856d6ceda86aa510894fa3527eac7989f0ef6e87b5721c58737a06c38 aarch64-linux-musl-cross.tgz +eb1db6f0f3c2bdbdbfb993d7ef7e2eeef82ac1259f6a6e1757c33a97dbcef3ad x86_64-linux-musl-native.tgz