unos-repository

APK repository for unos
git clone git://git.finwo.net/misc/unos-repository
Log | Files | Refs | README

commit da21c3314f47901983ba8f68bb17da89e360b231
parent 40626fe3a9ed50ec119acc41f0965462b4868120
Author: finwo <finwo@pm.me>
Date:   Fri, 18 Sep 2026 17:56:26 +0200

No more /opt/rescue workaround, full ownership of img

Diffstat:
Amk/make-img.sh | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmk/rootfs.sh | 2+-
Mmk/run-qemu.sh | 7+++++++
Mpackages/busybox/template | 14++++++++++----
Apackages/curl/files/post-install | 6++++++
Apackages/curl/template | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/dosfstools/template | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/smartmontools/template | 4++++
8 files changed, 311 insertions(+), 5 deletions(-)

diff --git a/mk/make-img.sh b/mk/make-img.sh @@ -0,0 +1,144 @@ +#!/bin/sh +# mk/make-img.sh - build a bootable UNOS image that carries its own installer. +# +# ./mk/make-img.sh # build with defaults +# ./mk/make-img.sh --version 0.1.0 --size 3072 +# ./mk/make-img.sh --authorized-keys ~/.ssh/id_ed25519.pub +# +# Output: build/work/unos-<version>-x86_64.img +# GPT + ESP (FAT, EFI/BOOT/BOOTX64.EFI) + ext4 root, bootable by firmware +# with no -kernel shortcut, carrying /opt/unos/unos-<version>-x86_64.bin. +# +# WHY A VM BUILDS THIS +# -------------------- +# Laying down a partitioned, bootable image needs loop devices, mount, and +# grub-install into a mounted ESP -- all of which need real root. We have none +# (user namespaces cannot set up loop devices). So instead of reimplementing +# partitioning on the host with mtools and grub-mkimage, this boots a UNOS live +# VM and runs installer/install.sh against a blank second disk. That path is +# already covered by tests/integration/test_qemu_install.sh, so the image +# builder and the installer cannot drift apart: fixing one fixes the other. +# +# WHY THE PAYLOAD IS COPIED IN AFTERWARDS +# --------------------------------------- +# The .bin contains a tarball of the rootfs. If the rootfs being packed already +# contained the .bin, the image would contain a copy of itself -- so the +# installer is built from the plain rootfs, installed, and only then copied +# onto the installed system as a separate step. +set -eu + +HERE=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "${HERE}/.." && pwd) +BUILD="${ROOT}/build/work" + +VERSION="${UNOS_VERSION:-0.1.0}" +SIZE_MB=3072 +OUT="" +AUTH_KEYS="" +PORT="${MAKEIMG_SSH_PORT:-2295}" + +while [ $# -gt 0 ]; do + case "$1" in + --version) VERSION="$2"; shift ;; + --out) OUT="$2"; shift ;; + --size) SIZE_MB="$2"; shift ;; + --authorized-keys) AUTH_KEYS="$2"; shift ;; + -h|--help) sed -n '2,12p' "$0"; exit 0 ;; + *) echo "make-img.sh: unknown argument: $1" >&2; exit 1 ;; + esac + shift +done + +[ -n "${OUT}" ] || OUT="${BUILD}/unos-${VERSION}-x86_64.img" +BIN="${BUILD}/unos-${VERSION}-x86_64.bin" +LIVE_LOG="${BUILD}/make-img-live.log" + +msg() { printf '==> %s\n' "$*"; } +die() { printf 'make-img.sh: error: %s\n' "$*" >&2; exit 1; } + +cleanup() { + for p in $(pgrep -f 'qemu-sys[t]em-x86_64' 2>/dev/null); do kill "$p" 2>/dev/null || true; done +} +trap cleanup EXIT + +[ -d "${ROOT}/rootfs" ] || die "no rootfs/; run ./mk/rootfs.sh first" + +KEY=$(ls "${HOME}"/.ssh/id_ed25519 2>/dev/null | head -1 || true) +[ -n "${KEY}" ] || KEY=$(ls "${HOME}"/.ssh/*.pub 2>/dev/null | head -1 | sed 's/.pub$//' || true) +[ -n "${KEY}" ] && [ -f "${KEY}" ] || die "no usable ssh key in ~/.ssh (needed to drive the build VM)" + +ssh_vm() { + ssh -i "${KEY}" -p "${PORT}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + -o LogLevel=ERROR -o ConnectTimeout=5 -o BatchMode=yes root@localhost "$@" 2>&1 +} + +wait_ssh() { + i=1 + while [ "$i" -le 90 ]; do + if ssh_vm true >/dev/null 2>&1; then return 0; fi + sleep 1 + i=$((i + 1)) + done + return 1 +} + +# --- 1. the payload -------------------------------------------------------- +# Built from the plain rootfs. Optionally bake authorized_keys in, because an +# image nobody can log into is only useful over a serial console. +STAGE="${BUILD}/stage-img-root" +rm -rf "${STAGE}" +mkdir -p "${STAGE}" +msg "staging rootfs" +(cd "${ROOT}/rootfs" && tar -cf - .) | (cd "${STAGE}" && tar -xf -) + +if [ -n "${AUTH_KEYS}" ]; then + [ -f "${AUTH_KEYS}" ] || die "no such authorized_keys file: ${AUTH_KEYS}" + mkdir -p "${STAGE}/root/.ssh" + cat "${AUTH_KEYS}" > "${STAGE}/root/.ssh/authorized_keys" + chmod 700 "${STAGE}/root/.ssh" + chmod 600 "${STAGE}/root/.ssh/authorized_keys" + msg "baked authorized_keys from ${AUTH_KEYS}" +else + msg "no --authorized-keys given: image will be console-only" +fi + +msg "building installer payload" +"${ROOT}/installer/mkinstaller.sh" --version "${VERSION}" \ + --rootfs "${STAGE}" --out "${BIN}" >/dev/null || die "mkinstaller failed" +[ -s "${BIN}" ] || die "installer payload not produced" +msg "payload: $(basename "${BIN}") ($(du -h "${BIN}" | cut -f1))" + +# --- 2. build VM ----------------------------------------------------------- +rm -f "${OUT}" "${BUILD}/ovmf-vars.fd" +cleanup + +msg "booting build VM (target ${SIZE_MB}M)" +UEFI=1 IMG_SIZE_MB=1536 QEMU_MEM=1536 QEMU_SSH_PORT="${PORT}" \ + TARGET_DISK="${OUT}" TARGET_SIZE_MB="${SIZE_MB}" \ + EXTRA_FILES="${BIN}:/root/unos.bin" \ + "${ROOT}/mk/run-qemu.sh" run > "${LIVE_LOG}" 2>&1 & + +wait_ssh || { tail -20 "${LIVE_LOG}" >&2; die "build VM did not come up"; } + +# --- 3. install onto the blank disk --------------------------------------- +msg "installing onto /dev/vdb" +out=$(ssh_vm 'sh /root/unos.bin --mode generic --disk /dev/vdb --force; echo "RC=$?"') +case "${out}" in + *RC=0*) ;; + *) printf '%s\n' "${out}" >&2; die "installer failed inside the build VM" ;; +esac + +# The installer payload is deliberately NOT copied into the installed root. +# The rootfs must be byte-identical whether it was installed normally or as a +# rescue image; embedding an 85MB copy of the installer would bloat every +# installed switch with a copy of itself, and would mean the image contains its +# own payload which contains the image. +ssh_vm 'sync' >/dev/null 2>&1 || true +cleanup + +[ -s "${OUT}" ] || die "no image produced" +msg "done: ${OUT} ($(du -h "${OUT}" | cut -f1))" +msg "" +msg " boot it: ./mk/run-qemu.sh boot-installed ${OUT} (UEFI=1)" +msg " install: curl -O http://.../unos-installer.bin && sh unos-installer.bin --disk /dev/sdX --force" +msg " write it: dd if=${OUT} of=/dev/sdX bs=4M conv=fsync" diff --git a/mk/rootfs.sh b/mk/rootfs.sh @@ -47,7 +47,7 @@ apk_ns "${APK}" --root "${ROOTFS}" --initdb \ echo "==> UNOS system (fully trusted from here on)" apk_ns "${APK}" --root "${ROOTFS}" \ --cache-dir "${CACHE}" --repository "${REPO}" \ - add base-files glibc busybox unos-firstboot tinyssh libmnl zlib openssl iproute2 apk-tools linux-longterm grub popt efivar efibootmgr unosd libstdc++ e2fsprogs gptfdisk smartmontools + add base-files glibc busybox unos-firstboot tinyssh libmnl zlib openssl iproute2 apk-tools linux-longterm grub popt efivar efibootmgr unosd libstdc++ e2fsprogs gptfdisk smartmontools dosfstools curl # Deterministic loader cache: package post-installs refresh it during the # transaction, but assembly must not depend on script-execution order. diff --git a/mk/run-qemu.sh b/mk/run-qemu.sh @@ -148,12 +148,19 @@ run_qemu() { boot_installed() { disk="$1" [ -f "${disk}" ] || { echo "run-qemu.sh: no such image: ${disk}" >&2; exit 1; } + tgt="" + if [ -n "${TARGET_DISK}" ]; then + [ -f "${TARGET_DISK}" ] || truncate -s "${TARGET_SIZE_MB}M" "${TARGET_DISK}" + tgt="-drive file=${TARGET_DISK},format=raw,if=virtio" + echo " target: ${TARGET_DISK} (/dev/vdb, ${TARGET_SIZE_MB}M)" + fi echo "==> booting installed disk ${disk} (no -kernel; firmware -> GRUB -> UNOS)" echo " uefi: ${UEFI}" # shellcheck disable=SC2086 exec qemu-system-x86_64 -enable-kvm -m "${QEMU_MEM}" \ $(uefi_args) \ -drive file="${disk}",format=raw,if=virtio \ + ${tgt} \ -nographic \ -netdev user,id=net0,hostfwd=tcp::${QEMU_SSH_PORT}-:22 \ -device e1000,netdev=net0 diff --git a/packages/busybox/template b/packages/busybox/template @@ -1,7 +1,7 @@ # Template file for 'busybox' pkgname=busybox version=1.36.1 -revision=7 +revision=8 short_desc="Swiss Army knife of embedded Linux utilities" maintainer="finwo <finwo@pm.me>" license="GPL-2.0-only" @@ -28,16 +28,22 @@ depends= # udhcpd (dnsmasq covers the server side), httpd/inetd (tcpsvd is our # super-server), IFUP (unosd owns /etc/network/interfaces via unosc, busybox # ifupdown would conflict and its `iface <name>` parser is for old -# `iface <name> inet static` only -- cumulus `iface <name>` fails there). +# `iface <name> inet static` only -- cumulus `iface <name>` fails there), +# MKFS_VFAT/MKDOSFS (dosfstools owns mkfs.vfat; two providers of one command +# resolved by PATH order is a trap, and busybox's variant warns about +# codepage conversion on every run because it wants iconv data our +# C.UTF-8-only glibc does not ship. dosfstools also brings fsck.fat, which +# busybox has no equivalent of at all -- and the ESP is the one partition +# whose corruption stops the switch booting). # Untouched (defconfig default) until their design lands: SYSLOGD/KLOGD. # No checked-in .config on purpose: defconfig + CFG_* is the source of truth # (this kconfig has no savedefconfig anyway); the assertions below enforce it. CFG_ON="INIT HALT POWEROFF REBOOT GETTY LOGIN MOUNT UMOUNT SWITCH_ROOT ASH ASH_JOB_CONTROL RUNSV RUNSVDIR SV SVLOGD CHPST SETUIDGID TCPSVD UDHCPC" -CFG_OFF="IP FEATURE_IP_ADDRESS FEATURE_IP_LINK FEATURE_IP_ROUTE FEATURE_IP_RULE FEATURE_IP_TUNNEL TC LDCONFIG UNLZMA LZCAT LZMA UNXZ XZCAT XZ UDHCPD HTTPD INETD IFUP IFDOWN" +CFG_OFF="IP FEATURE_IP_ADDRESS FEATURE_IP_LINK FEATURE_IP_ROUTE FEATURE_IP_RULE FEATURE_IP_TUNNEL TC LDCONFIG UNLZMA LZCAT LZMA UNXZ XZCAT XZ UDHCPD HTTPD INETD IFUP IFDOWN MKFS_VFAT MKDOSFS" # applet names as `busybox --list` prints them REQUIRE="init halt poweroff reboot getty login mount umount switch_root ash runsv runsvdir sv svlogd chpst setuidgid tcpsvd udhcpc" -FORBID="ip tc ldconfig unlzma lzcat lzma unxz xzcat xz udhcpd httpd inetd ifup ifdown" +FORBID="ip tc ldconfig unlzma lzcat lzma unxz xzcat xz udhcpd httpd inetd ifup ifdown mkfs.vfat mkdosfs" set_config() { # $1 = symbol, $2 = y|n diff --git a/packages/curl/files/post-install b/packages/curl/files/post-install @@ -0,0 +1,6 @@ +#!/bin/sh +# curl post-install: refresh the loader cache so libcurl resolves. +# Every UNOS library package carries this (apk runs scripts per package at +# install time; a single glibc-side run would be order-dependent). Absolute +# path on purpose (see glibc post-install). +exec /sbin/ldconfig diff --git a/packages/curl/template b/packages/curl/template @@ -0,0 +1,84 @@ +# Template file for 'curl' +pkgname=curl +version=8.18.0 +revision=0 +short_desc="Command line HTTP/HTTPS client and libcurl" +maintainer="finwo <finwo@pm.me>" +license="curl" +homepage="https://curl.se/" +distfiles="https://curl.se/download/curl-${version}.tar.xz" +checksum=40df79166e74aa20149365e11ee4c798a46ad57c34e4f68fd13100e2c9a91946 +depends="glibc busybox openssl zlib" +makedepends="openssl zlib" + +# Notes: +# - Why: a rescue image needs to pull things onto the box -- an installer +# payload, a replacement config, a kernel. tinyssh is daemon-only (upstream +# ships no client, so no scp/sftp either), which left busybox wget as the +# sole inbound path. wget is fine until you need TLS with a real CA store, +# redirects, or a useful error message when a fetch fails at 3am. +# - Protocol surface is cut hard: this is a recovery tool, not a general +# purpose client. HTTP/HTTPS/FTP stay; everything else goes. Each disabled +# protocol is one less parser reachable from the network. +# - OpenSSL backend (we already package it) rather than bundling another TLS +# stack. CA bundle comes from the system path, not baked in here. +# - libcurl ships too: it is the same build, and shipping only the binary +# would mean statically linking it for no reason. + +do_configure() { + cd "${WRKSRC}" + ./configure \ + --prefix=/usr \ + --libdir=/usr/lib \ + --disable-static \ + --enable-shared \ + --with-openssl \ + --with-zlib \ + --without-libpsl \ + --without-libidn2 \ + --without-brotli \ + --without-zstd \ + --without-nghttp2 \ + --without-libssh2 \ + --disable-ldap \ + --disable-ldaps \ + --disable-rtsp \ + --disable-dict \ + --disable-telnet \ + --disable-tftp \ + --disable-pop3 \ + --disable-imap \ + --disable-smb \ + --disable-smtp \ + --disable-gopher \ + --disable-mqtt \ + --disable-manual \ + --enable-optimize +} + +do_build() { + cd "${WRKSRC}" + make -j"$(nproc)" +} + +do_install() { + cd "${WRKSRC}" + make install DESTDIR="${DESTDIR}" + + # Sysroot seed: anything later linking libcurl builds against ours. + make install DESTDIR="${SYSROOT}" + + rm -f "${DESTDIR}"/usr/lib/*.la "${SYSROOT}"/usr/lib/*.la + rm -rf "${DESTDIR}/usr/share/man" "${DESTDIR}/usr/share/doc" + # curl-config is a build-time helper for consumers, not a switch tool. + rm -f "${DESTDIR}/usr/bin/curl-config" + + [ -x "${DESTDIR}/usr/bin/curl" ] || die "curl: no curl binary" + [ -e "${DESTDIR}/usr/lib/libcurl.so" ] || die "curl: no libcurl" + # Prove TLS is actually wired up; a curl without HTTPS is a trap in a + # rescue image, and configure will happily build one. + readelf -d "${DESTDIR}/usr/bin/curl" | grep -q 'libcurl\.so' \ + || die "curl: binary does not link libcurl" + readelf -d "${DESTDIR}/usr/lib/libcurl.so."* | grep -q 'libssl\.so' \ + || die "curl: libcurl not linked against OpenSSL (no HTTPS)" +} diff --git a/packages/dosfstools/template b/packages/dosfstools/template @@ -0,0 +1,55 @@ +# Template file for 'dosfstools' +pkgname=dosfstools +version=4.2 +revision=0 +short_desc="FAT filesystem utilities (mkfs.fat, fsck.fat)" +maintainer="finwo <finwo@pm.me>" +license="GPL-3.0-or-later" +homepage="https://github.com/dosfstools/dosfstools" +distfiles="https://github.com/dosfstools/dosfstools/releases/download/v${version}/dosfstools-${version}.tar.gz" +checksum=64926eebf90092dca21b14259a5301b7b98e7b1943e8a201c7d726084809b527 +depends="glibc busybox" + +# Notes: +# - Why: the ESP is FAT, so the installer creates one on every install and a +# rescue image needs to be able to REPAIR one. busybox has an mkfs.vfat +# applet but no fsck for FAT at all, which means a corrupt ESP -- the single +# partition that decides whether the switch boots -- was unfixable in place. +# - busybox's MKFS_VFAT applet is disabled in packages/busybox/template so +# there is exactly one mkfs.vfat on the system. Two providers of the same +# command name resolved by PATH order is the kind of ambiguity that only +# shows up when the two behave differently; busybox's also emits codepage +# warnings on every run because it wants iconv data our glibc does not carry. +# - iconv/nls off: the codepage machinery is for long-filename charset +# conversion, which an ESP does not need and which our C.UTF-8-only glibc +# cannot satisfy anyway. + +do_configure() { + cd "${WRKSRC}" + ./configure \ + --prefix=/usr \ + --sbindir=/usr/sbin \ + --mandir=/usr/share/man \ + --enable-compat-symlinks \ + --without-udev \ + --without-iconv +} + +do_build() { + cd "${WRKSRC}" + make -j"$(nproc)" +} + +do_install() { + cd "${WRKSRC}" + make install DESTDIR="${DESTDIR}" + + rm -rf "${DESTDIR}/usr/share/doc" "${DESTDIR}/usr/share/locale" + + # --enable-compat-symlinks gives the names callers actually use: + # installer/install.sh invokes mkfs.vfat, and fsck.vfat is what anyone + # reaching for a FAT repair will type. + for t in mkfs.fat fsck.fat mkfs.vfat fsck.vfat fatlabel; do + [ -e "${DESTDIR}/usr/sbin/${t}" ] || die "dosfstools: missing ${t}" + done +} diff --git a/packages/smartmontools/template b/packages/smartmontools/template @@ -51,6 +51,9 @@ do_install() { rm -f "${DESTDIR}"/etc/smartd.conf rm -f "${DESTDIR}"/usr/share/man/man8/smartd.8 \ "${DESTDIR}"/usr/share/man/man5/smartd.conf.5 + # smartd_warning.sh is smartd's notification helper; without smartd it is + # an orphan that only looks like a supported hook. + rm -f "${DESTDIR}"/etc/smartd_warning.sh rm -rf "${DESTDIR}"/etc/smartd_warning.d \ "${DESTDIR}"/usr/share/smartmontools \ "${DESTDIR}"/usr/libexec \ @@ -60,6 +63,7 @@ do_install() { [ -x "${DESTDIR}/usr/sbin/smartctl" ] || die "smartmontools: no smartctl" [ ! -e "${DESTDIR}/usr/sbin/smartd" ] || die "smartmontools: smartd should not ship" + [ ! -e "${DESTDIR}/etc/smartd_warning.sh" ] || die "smartmontools: smartd helper left behind" readelf -d "${DESTDIR}/usr/sbin/smartctl" | grep -q 'libstdc++\.so\.6' \ || die "smartmontools: smartctl does not link libstdc++.so.6" }