commit 9427b1657ea91c724eb631761002facaa7a1e6c5
parent 478dbe703a4bbc6bbc152eb5352aacd3fdd71be1
Author: finwo <finwo@pm.me>
Date: Thu, 17 Sep 2026 17:55:30 +0200
Bootable testing
Diffstat:
5 files changed, 179 insertions(+), 4 deletions(-)
diff --git a/mk/run-qemu.sh b/mk/run-qemu.sh
@@ -0,0 +1,121 @@
+#!/bin/sh
+# mk/run-qemu.sh - build a bootable UNOS image with injected SSH keys and run it in QEMU.
+# No hardcoding of key names: discovers pubkeys at runtime.
+set -eu
+
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "${HERE}/.." && pwd)
+BUILD="${ROOT}/build/work"
+STAGE="${BUILD}/stage-qemu-root"
+IMG="${BUILD}/unos-qemu.img"
+IMG_SIZE_MB="${IMG_SIZE_MB:-512}"
+QEMU_SSH_PORT="${QEMU_SSH_PORT:-2222}"
+QEMU_MEM="${QEMU_MEM:-512}"
+KERNEL="${ROOT}/rootfs/boot/vmlinuz"
+
+# --- collect authorized_keys (automated, no hardcoding) ---
+collect_keys() {
+ tmp=$(mktemp)
+ # all .pub files
+ for f in "${HOME}"/.ssh/*.pub; do
+ [ -e "$f" ] || continue
+ cat "$f" >> "$tmp" 2>/dev/null || true
+ done
+ # authorized_keys if present (may contain certs, deduplicated via sort -u)
+ if [ -f "${HOME}/.ssh/authorized_keys" ]; then
+ cat "${HOME}/.ssh/authorized_keys" >> "$tmp" 2>/dev/null || true
+ fi
+ # keep only non-empty, non-comment lines? Keep as-is but deduplicate
+ # filter blank lines, let sort handle dedupe
+ if [ -s "$tmp" ]; then
+ sort -u "$tmp" -o "$tmp"
+ # drop empty lines
+ grep -v '^[[:space:]]*$' "$tmp" > "$tmp.filtered" 2>/dev/null || true
+ mv "$tmp.filtered" "$tmp"
+ fi
+ # require at least one key
+ if [ ! -s "$tmp" ]; then
+ echo "run-qemu.sh: no pubkeys found in ~/.ssh/*.pub or ~/.ssh/authorized_keys" >&2
+ rm -f "$tmp"
+ return 1
+ fi
+ echo "$tmp"
+}
+
+# --- stage rootfs with injected keys ---
+stage_rootfs() {
+ keys_file="$1"
+ echo "==> staging rootfs"
+ rm -rf "${STAGE}"
+ mkdir -p "${STAGE}"
+ # copy via tar to preserve perms; rootfs is unprivileged dir tree
+ (cd "${ROOT}/rootfs" && tar -cf - .) | (cd "${STAGE}" && tar -xf -)
+
+ # inject root's authorized_keys (automated)
+ mkdir -p "${STAGE}/root/.ssh"
+ cat "$keys_file" > "${STAGE}/root/.ssh/authorized_keys"
+ chmod 700 "${STAGE}/root/.ssh"
+ chmod 600 "${STAGE}/root/.ssh/authorized_keys"
+ chown -R 0:0 "${STAGE}/root/.ssh" 2>/dev/null || true
+ echo "==> injected $(wc -l < "$keys_file") key(s) into /root/.ssh/authorized_keys"
+}
+
+build_image() {
+ echo "==> building ${IMG} (${IMG_SIZE_MB}M)"
+ rm -f "${IMG}"
+ # Injected authorized_keys is created as $USER, fix ownership via fake-root ns
+ # before mkfs (which copies ownership verbatim).
+ if command -v unshare >/dev/null 2>&1; then
+ unshare --user --map-root-user --mount -- sh -c '
+ chown -R 0:0 "$1/root/.ssh" 2>/dev/null || true
+ mkfs.ext4 -L UNOS -m 0 -d "$1" "$2" "$3" >/dev/null
+ ' sh "${STAGE}" "${IMG}" "${IMG_SIZE_MB}M" 2>/dev/null || mkfs.ext4 -L UNOS -m 0 -d "${STAGE}" "${IMG}" "${IMG_SIZE_MB}M" >/dev/null
+ else
+ mkfs.ext4 -L UNOS -m 0 -d "${STAGE}" "${IMG}" "${IMG_SIZE_MB}M" >/dev/null
+ fi
+ echo "==> image ready: ${IMG} ($(du -h "${IMG}" | cut -f1))"
+ # sanity: label
+ tune2fs -l "${IMG}" 2>/dev/null | grep -E "Filesystem volume name|Block count" | head -2
+}
+
+run_qemu() {
+ [ -f "${KERNEL}" ] || { echo "run-qemu.sh: kernel not found: ${KERNEL} (run mk/rootfs.sh first)" >&2; exit 1; }
+ [ -f "${IMG}" ] || { echo "run-qemu.sh: image not found: ${IMG}" >&2; exit 1; }
+ echo "==> launching QEMU (ssh -p ${QEMU_SSH_PORT} root@localhost)"
+ echo " kernel: ${KERNEL}"
+ echo " drive: ${IMG}"
+ echo " mem: ${QEMU_MEM}M"
+ echo " press Ctrl-a c then 'quit' to exit QEMU"
+ echo ""
+ # shellcheck disable=SC2086
+ exec qemu-system-x86_64 -enable-kvm -m "${QEMU_MEM}" \
+ -kernel "${KERNEL}" \
+ -drive file="${IMG}",format=raw,if=virtio \
+ -append "console=ttyS0 root=/dev/vda rw" \
+ -nographic \
+ -netdev user,id=net0,hostfwd=tcp::${QEMU_SSH_PORT}-:22 \
+ -device e1000,netdev=net0
+}
+
+# --- main ---
+mode="${1:-run}"
+case "$mode" in
+ build)
+ kf=$(collect_keys)
+ stage_rootfs "$kf"
+ build_image
+ rm -f "$kf"
+ echo "==> done (build only). Run '$0 run' to start QEMU."
+ ;;
+ run|"")
+ kf=$(collect_keys)
+ stage_rootfs "$kf"
+ build_image
+ rm -f "$kf"
+ run_qemu
+ ;;
+ *)
+ echo "usage: $0 [build|run]" >&2
+ exit 1
+ ;;
+esac
diff --git a/packages/base-files/files/inittab b/packages/base-files/files/inittab
@@ -0,0 +1,4 @@
+::sysinit:/etc/init.d/rcS
+::respawn:/sbin/getty -L ttyS0 115200 vt100
+::ctrlaltdel:/sbin/reboot
+::shutdown:/bin/umount -a -r
diff --git a/packages/base-files/files/rcS b/packages/base-files/files/rcS
@@ -0,0 +1,47 @@
+#!/bin/sh
+exec >/dev/console 2>&1
+echo "rcS: mounting pseudo-filesystems"
+mount -t proc proc /proc 2>/dev/null || true
+mount -t sysfs sys /sys 2>/dev/null || true
+mount -t devtmpfs devtmpfs /dev 2>/dev/null || true
+mkdir -p /dev/pts 2>/dev/null || true
+mount -t devpts devpts /dev/pts -o gid=5,mode=620 2>/dev/null || mount -t devpts devpts /dev/pts 2>/dev/null || true
+[ -c /dev/ptmx ] || mknod /dev/ptmx c 5 2 2>/dev/null || true
+chmod 666 /dev/ptmx 2>/dev/null || true
+mkdir -p /run 2>/dev/null || true
+if [ -f /etc/hostname ]; then
+ hostname -F /etc/hostname 2>/dev/null || hostname unos 2>/dev/null || true
+ echo "rcS: hostname $(hostname 2>/dev/null || cat /etc/hostname)"
+fi
+echo "rcS: bringing up loopback"
+ip link set lo up 2>/dev/null || ifconfig lo up 2>/dev/null || true
+echo "rcS: bringing up eth0"
+ip link set eth0 up 2>&1 | head -1
+sleep 1
+# qemu user-mode net: dhcp via udhcpc if available, else static fallback
+if command -v udhcpc >/dev/null 2>&1; then
+ udhcpc -i eth0 -q 2>&1 || true
+ # fallback if dhcp left no address
+ ip addr show eth0 2>/dev/null | grep -q "inet " || {
+ echo "rcS: dhcp failed, using static 10.0.2.15/24"
+ ip addr add 10.0.2.15/24 dev eth0 2>/dev/null || ifconfig eth0 10.0.2.15 netmask 255.255.255.0 2>/dev/null || true
+ ip route add default via 10.0.2.2 2>/dev/null || route add default gw 10.0.2.2 2>/dev/null || true
+ echo "nameserver 10.0.2.3" > /etc/resolv.conf 2>/dev/null || true
+ }
+else
+ ip addr add 10.0.2.15/24 dev eth0 2>/dev/null || ifconfig eth0 10.0.2.15 netmask 255.255.255.0 2>/dev/null || true
+ ip link set eth0 up 2>/dev/null || true
+ ip route add default via 10.0.2.2 2>/dev/null || true
+fi
+echo "rcS: network ready"
+ip addr show 2>&1 | head -20
+# start supervision (runsvdir manages /etc/sv/*)
+if [ -d /etc/sv ]; then
+ echo "rcS: starting runsvdir"
+ runsvdir /etc/sv &
+fi
+if [ -x /etc/sv/tinyssh/run ]; then
+ echo "rcS: tinyssh will be supervised"
+else
+ echo "rcS: no tinyssh service found"
+fi
diff --git a/packages/base-files/template b/packages/base-files/template
@@ -1,7 +1,7 @@
# Template file for 'base-files'
pkgname=base-files
version=0.1.0
-revision=3
+revision=4
short_desc="UNOS filesystem skeleton - passwd, profile, nsswitch, fstab"
maintainer="finwo <finwo@pm.me>"
license="GPL-2.0-only"
@@ -11,8 +11,8 @@ depends=
# Own skeleton, nothing from any upstream distro. Traditional split-/usr
# layout (matches what busybox `make install` produces: /bin /sbin
# /usr/bin /usr/sbin as real dirs - no usrmerge). Deliberately minimal:
-# no inittab yet (PID-1 design pending), no resolv.conf (runtime-provided
-# via DHCP), no /run or /tmp (tmpfs mounts created at boot).
+# no resolv.conf (runtime-provided via DHCP), no /run or /tmp (tmpfs mounts
+# created at boot). Init is busybox init + runit via inittab/rcS.
do_install() {
vinstall ${FILESDIR}/passwd 644 etc
vinstall ${FILESDIR}/group 644 etc
@@ -25,6 +25,8 @@ do_install() {
vinstall ${FILESDIR}/ld.so.conf 644 etc
vinstall ${FILESDIR}/fstab 644 etc
vinstall ${FILESDIR}/os-release 644 etc
+ vinstall ${FILESDIR}/inittab 644 etc
+ vinstall ${FILESDIR}/rcS 755 etc/init.d rcS
install -d -m700 "${DESTDIR}/root"
install -d "${DESTDIR}/etc/ld.so.conf.d"
# standard mount points (fstab references /run and /tmp; init mounts the
diff --git a/packages/tinyssh/template b/packages/tinyssh/template
@@ -1,7 +1,7 @@
# Template file for 'tinyssh'
pkgname=tinyssh
version=20260906
-revision=3
+revision=4
short_desc="Small SSH server"
maintainer="finwo <finwo@pm.me>"
license="CC0-1.0"
@@ -42,4 +42,5 @@ do_install() {
# owned (empty) so the keydir parent always exists; the run script also
# mkdir -p's defensively for wiped-state recovery
install -d "${DESTDIR}/etc/tinyssh"
+ install -d "${DESTDIR}/var/log/tinyssh"
}