commit 3cf5df4bd950279e1a7708885215e059c36c63ce parent e83fb73fcd0f58cd669baefff66b66522c29e7c6 Author: finwo <finwo@pm.me> Date: Mon, 14 Sep 2026 09:39:54 +0200 add base-files,busybox,glibc packages Diffstat:
21 files changed, 328 insertions(+), 6 deletions(-)
diff --git a/mk/README.md b/mk/README.md @@ -27,7 +27,9 @@ The whole of `build/` is generated output. Only `mk/` is source. sets `UNOS_SIGN_KEY` or writes `.sign-key` itself); `list` shows local keys and marks the active one. apk mandates RSA -- supercop-for-apk is refused. -C and shell only. No Python anywhere, build host included. +C and shell only. Python is banned from our tooling and from every rootfs; +the sole exception is an upstream package whose own build mandates it +(glibc needs python3 to build) - used for that build only, never shipped. ## Template contract @@ -37,9 +39,10 @@ Shell fragment, not a script. Variables: `pkgname`, `version`, `revision` (space-separated sha256, same order -- mandatory when `distfiles` is set), `depends`, `provides`, `replaces` (space-separated, may be empty). -Optional phase overrides: `do_build`, `do_install` (default: no-op). -Environment and helpers provided: `DESTDIR`, `WRKSRC`, `FILESDIR`, -`vinstall <file> <mode> <targetdir> [name]`, `vmkdir <dir>`, `msg`, `die`. +Optional phase overrides: `do_configure`, `do_build`, `do_install` (default: +no-op). Environment and helpers provided: `DESTDIR`, `WRKSRC`, `FILESDIR`, `WORK` +(scratch root for out-of-tree builds), `vinstall <file> <mode> <targetdir> +[name]`, `vmkdir <dir>`, `msg`, `die`. Control-script pickup: `pre-install`, `post-install`, `pre-deinstall`, `post-deinstall`, `pre-upgrade`, `post-upgrade`, `trigger` in `files/` are @@ -75,8 +78,52 @@ index. Current keys (dev estate, rotate before any release): markers (only the data segment terminates the archive). - `datahash` is plain sha256 hex of `data.tar.gz` -- no `Q1` prefix (that prefix form lives in indexes, where `apk index` computes it itself). +- Per-file `APK-TOOLS.checksum.SHA1` records are required for symlinks too + (v2 warns otherwise): symlinks hash the target string, regular files their + content (verified against `abuild-tar.c`). ## Host dependencies sh, cc, curl or wget, tar, gzip, openssl, sha256sum, install, patch, apk-tools (validation only: `apk verify`, `apk index`, `--root` installs). +`unshare` (user namespaces) for `chroot.sh`. + +## Rootfs (`rootfs.sh`, `chroot.sh`) + +`mk/rootfs.sh [--force]` assembles `rootfs/` (ignored) with static apk +alone - no xbps, no foreign content: `unos-keys` via one explicit +`--allow-untrusted` (the trust seed), then `base-files`, `glibc`, +`busybox`, `unos-firstboot` fully trusted. +`mk/chroot.sh [--] <cmd>` enters it via `unshare --user --map-root-user +--mount --pid` with host `/proc` `/sys` `/dev` bound; everything evaporates +on exit, no root needed. Standing acceptance: ash runs, required applets +resolve, `ip` absent, `runsvdir` supervises (`sv status`/`sv stop`), +`tcpsvd` accepts, `firstboot` is `sh -n` clean. + +## Host apk-tools v3 (bootstrap input, pinned) + +Upstream `apk-tools` v3.0.8, tarball +`https://gitlab.alpinelinux.org/alpine/apk-tools/-/archive/v3.0.8/apk-tools-v3.0.8.tar.gz` +sha256 `e81c64a6e7c3806d45d4622c253e757aee970be84048554fa0eed79310e9453e`, +fetched once into `build/work/sources/`. Built in-tree **with muon, never +meson** (Python is banned from our tooling; upstream supports muon): + +- dynamic, prefix `build/host/` (`-Dlua=disabled -Dpython=disabled + -Dhelp=disabled`, rpath `$ORIGIN/../lib`): `build/host/bin/apk` for index + generation and validation; +- static (`build/host/bin/apk.static`): the future target binary - no + libcrypto needed on the switch. glibc-static NSS caveats apply in theory, + irrelevant to package ops. + +Muon itself is pinned: v0.6.0, +`https://github.com/muon-build/muon/archive/refs/tags/0.6.0.tar.gz`, +sha256 `5300e58c4b4d43e3026856004c79d746075aaa9d9e66d76ba9f32ce249495b81`, +bootstrapped (`bootstrap.sh`, pure C) into `build/host/bin/muon`. muon is +GPL-3.0 - fine as a host-only build tool (same standing as GCC); it never +enters any package or rootfs. + +Our v2 artifacts verify and install cleanly under both v2.14 (host) and +v3.0.8, untrusted and fully trusted (signed index + seeded key). v3 needs +`--usermode` for non-root `--root` installs. This becomes a real package +template once the toolchain work lands; until then the pin + hash above are +the record. diff --git a/mk/build.sh b/mk/build.sh @@ -96,11 +96,14 @@ if [ -n "${distfiles}" ]; then patch -d "${WRKSRC}" -p1 --no-backup-if-mismatch -i "${p}" done fi -export DESTDIR WRKSRC FILESDIR +export DESTDIR WRKSRC FILESDIR WORK # --- build + install phases (defaults: no-op) --- +command -v do_configure >/dev/null 2>&1 || do_configure() { :; } command -v do_build >/dev/null 2>&1 || do_build() { :; } command -v do_install >/dev/null 2>&1 || do_install() { :; } +msg "${pkgname}: do_configure" +do_configure msg "${pkgname}: do_build" do_build msg "${pkgname}: do_install" diff --git a/mk/chroot.sh b/mk/chroot.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# mk/chroot.sh - enter rootfs/ in a user+mount namespace and run a command. +# +# Usage: ./mk/chroot.sh [--] <cmd> [args...] (default: /bin/busybox sh) +# +# Unprivileged: unshare maps us to root inside, mounts are namespace-private +# and evaporate on exit. /proc /sys /dev come from the host via binds. +set -eu + +HERE=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "${HERE}/.." && pwd) +ROOTFS="${ROOT}/rootfs" + +[ -d "${ROOTFS}/bin" ] || { echo "chroot.sh: no rootfs (run mk/rootfs.sh)" >&2; exit 1; } +if [ "${1:-}" = "--" ]; then shift; fi +if [ $# = 0 ]; then set -- /bin/busybox sh; fi + +exec unshare --user --map-root-user --mount --propagation private --pid --fork \ + sh -c ' + R="$1"; shift + mount -t proc proc "$R/proc" + mount --rbind /sys "$R/sys" + mount --rbind /dev "$R/dev" + exec chroot "$R" "$@" + ' chroot-sh "${ROOTFS}" "$@" diff --git a/mk/keymgmt.sh b/mk/keymgmt.sh @@ -46,7 +46,7 @@ cmd_new() { done [ $# = 1 ] || die "usage: keymgmt.sh new [--type rsa|supercop] [--for apk|unos] <stem>" stem=$1 - valid_stem "${stem}" || die "bad stem (allowed: A–Z a–z 0–9 @ . _ -): ${stem}" + valid_stem "${stem}" || die "bad stem (allowed: A-Z a-z 0-9 @ . _ -): ${stem}" case "${type}" in rsa|supercop) ;; *) die "type must be rsa or supercop";; esac if [ -z "${for}" ]; then if [ "${type}" = rsa ]; then for=apk; else for=unos; fi diff --git a/mk/pax-tar.c b/mk/pax-tar.c @@ -119,6 +119,17 @@ static void sha1_final(sha1_t *c, uint8_t out[20]) { } } +static void sha1_buf_hex(const uint8_t *data, size_t n, char out[41]) { + sha1_t c; + sha1_init(&c); + sha1_update(&c, data, n); + uint8_t digest[20]; + sha1_final(&c, digest); + for (int i = 0; i < 20; i++) + sprintf(out + 2 * i, "%02x", digest[i]); + out[40] = '\0'; +} + static void sha1_file_hex(const char *path, char out[41]) { FILE *f = fopen(path, "rb"); if (!f) { @@ -384,6 +395,12 @@ int main(int argc, char **argv) { return 1; } target[n] = '\0'; + if (checksums) { + /* abuild-tar hashes the link target string for symlinks */ + char hex[41]; + sha1_buf_hex((uint8_t *)target, (size_t)n, hex); + write_pax_record(out, "APK-TOOLS.checksum.SHA1", hex, mtime); + } write_header(out, e->arc, e->st.st_mode, 0, mtime, '2', target); } else { fprintf(stderr, "pax-tar: unsupported file type: %s\n", e->full); diff --git a/mk/rootfs.sh b/mk/rootfs.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# mk/rootfs.sh - assemble rootfs/ for chroot testing. +# +# Usage: ./mk/rootfs.sh [--force] +# +# Pure apk: static apk installs unos-keys with one explicit +# --allow-untrusted (the trust seed), then everything else - base-files, +# glibc, busybox, unos-firstboot - installs fully trusted. No xbps, no +# foreign repo content anywhere in the loop. Refuses to clobber an +# existing rootfs/ unless --force. +set -eu + +HERE=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "${HERE}/.." && pwd) +ROOTFS="${ROOT}/rootfs" +APK="${ROOT}/build/host/bin/apk.static" +CACHE="${ROOT}/build/work/apk-cache" +REPO="file://${ROOT}/build/repo" + +if [ -d "${ROOTFS}" ]; then + [ "${1:-}" = "--force" ] || { echo "rootfs.sh: ${ROOTFS} exists (use --force)" >&2; exit 1; } + rm -rf "${ROOTFS}" +fi +mkdir -p "${ROOTFS}" "${CACHE}" + +echo "==> trust seed: unos-keys via explicit --allow-untrusted" +"${APK}" --root "${ROOTFS}" --initdb --usermode \ + --cache-dir "${CACHE}" --repository "${REPO}" \ + --allow-untrusted add unos-keys + +echo "==> UNOS system (fully trusted from here on)" +"${APK}" --root "${ROOTFS}" --usermode \ + --cache-dir "${CACHE}" --repository "${REPO}" \ + add base-files glibc busybox unos-firstboot + +echo "==> done: ${ROOTFS}" diff --git a/packages/base-files/files/fstab b/packages/base-files/files/fstab @@ -0,0 +1,5 @@ +proc /proc proc defaults 0 0 +sysfs /sys sysfs defaults 0 0 +devpts /dev/pts devpts gid=5,mode=620 0 0 +tmpfs /tmp tmpfs defaults 0 0 +tmpfs /run tmpfs defaults 0 0 diff --git a/packages/base-files/files/group b/packages/base-files/files/group @@ -0,0 +1,3 @@ +root:x:0: +tty:x:5: +nobody:x:65534: diff --git a/packages/base-files/files/hostname b/packages/base-files/files/hostname @@ -0,0 +1 @@ +unos diff --git a/packages/base-files/files/hosts b/packages/base-files/files/hosts @@ -0,0 +1,2 @@ +127.0.0.1 localhost +::1 localhost diff --git a/packages/base-files/files/ld.so.conf b/packages/base-files/files/ld.so.conf @@ -0,0 +1 @@ +include /etc/ld.so.conf.d/*.conf diff --git a/packages/base-files/files/nsswitch.conf b/packages/base-files/files/nsswitch.conf @@ -0,0 +1,9 @@ +passwd: files +group: files +shadow: files +hosts: files dns +services: files +networks: files +protocols: files +rpc: files +ethers: files diff --git a/packages/base-files/files/os-release b/packages/base-files/files/os-release @@ -0,0 +1,6 @@ +NAME="UNOS" +ID=unos +VERSION="0.1.0" +VERSION_ID="0.1.0" +PRETTY_NAME="UNOS 0.1.0" +HOME_URL="https://unos.finwo.dev" diff --git a/packages/base-files/files/passwd b/packages/base-files/files/passwd @@ -0,0 +1,2 @@ +root:x:0:0:root:/root:/bin/ash +nobody:x:65534:65534:nobody:/nonexistent:/bin/false diff --git a/packages/base-files/files/profile b/packages/base-files/files/profile @@ -0,0 +1,4 @@ +# UNOS default profile +export PATH=/bin:/sbin:/usr/bin:/usr/sbin +export PS1='\u@\h:\w\$ ' +umask 022 diff --git a/packages/base-files/files/shadow b/packages/base-files/files/shadow @@ -0,0 +1,2 @@ +root:!:19000:0:99999:7::: +nobody:!:19000:0:99999:7::: diff --git a/packages/base-files/files/shells b/packages/base-files/files/shells @@ -0,0 +1,2 @@ +/bin/ash +/bin/sh diff --git a/packages/base-files/template b/packages/base-files/template @@ -0,0 +1,34 @@ +# Template file for 'base-files' +pkgname=base-files +version=0.1.0 +revision=2 +short_desc="UNOS filesystem skeleton - passwd, profile, nsswitch, fstab" +maintainer="finwo <finwo@pm.me>" +license="GPL-2.0-only" +homepage="https://unos.finwo.dev" +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). +do_install() { + vinstall ${FILESDIR}/passwd 644 etc + vinstall ${FILESDIR}/group 644 etc + vinstall ${FILESDIR}/shadow 640 etc + vinstall ${FILESDIR}/shells 644 etc + vinstall ${FILESDIR}/profile 644 etc + vinstall ${FILESDIR}/hostname 644 etc + vinstall ${FILESDIR}/hosts 644 etc + vinstall ${FILESDIR}/nsswitch.conf 644 etc + vinstall ${FILESDIR}/ld.so.conf 644 etc + vinstall ${FILESDIR}/fstab 644 etc + vinstall ${FILESDIR}/os-release 644 etc + install -d -m700 "${DESTDIR}/root" + install -d "${DESTDIR}/etc/ld.so.conf.d" + # standard mount points (fstab references /run and /tmp; init mounts the + # rest - the dirs must exist first) + install -d "${DESTDIR}/proc" "${DESTDIR}/sys" "${DESTDIR}/dev" + install -d "${DESTDIR}/dev/pts" "${DESTDIR}/tmp" "${DESTDIR}/run" +} diff --git a/packages/busybox/template b/packages/busybox/template @@ -0,0 +1,74 @@ +# Template file for 'busybox' +pkgname=busybox +version=1.36.1 +revision=3 +short_desc="UNOS userland - init, supervision, shell, coreutils" +maintainer="finwo <finwo@pm.me>" +license="GPL-2.0-only" +homepage="https://busybox.net" +distfiles="https://busybox.net/downloads/busybox-1.36.1.tar.bz2" +checksum=b8cc24c9574d809e7279c3be349795c5d5ceb6fdf19ca709f80cde50e47de314 +depends= + +# Applet policy (locked in PLAN.md section 10, enforced by assertions in +# do_build - a missing symbol fails the build, never the boot): +# +# ON: init sequence + supervision + shell + connection handling, i.e. the +# whole reason busybox is here, plus console login and mgmt DHCP. +# (INIT/HALT/POWEROFF/REBOOT/GETTY/LOGIN/MOUNT/UMOUNT/SWITCH_ROOT are +# defconfig defaults; listing them makes the requirement explicit.) +# OFF: IP applets (iproute2 is the only `ip`), TC (iproute2's `tc` is +# full-featured; busybox's references kernel-removed CBQ UAPI and does not +# build against modern headers), udhcpd (dnsmasq covers the server side), +# httpd/inetd (tcpsvd is our super-server). +# 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 UDHCPD HTTPD INETD" + +# 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 udhcpd httpd inetd" + +set_config() { + # $1 = symbol, $2 = y|n + if grep -q "^CONFIG_$1=" .config 2>/dev/null || grep -q "^# CONFIG_$1 is not set$" .config 2>/dev/null; then + sed -i -e "s/^# CONFIG_$1 is not set\$/CONFIG_$1=$2/" -e "s/^CONFIG_$1=.*\$/CONFIG_$1=$2/" .config + else + [ "$2" = y ] && echo "CONFIG_$1=y" >> .config || echo "# CONFIG_$1 is not set" >> .config + fi +} + +do_configure() { + cd "${WRKSRC}" + make defconfig >/dev/null + for c in ${CFG_ON}; do set_config "${c}" y; done + for c in ${CFG_OFF}; do set_config "${c}" n; done + # this kconfig has no olddefconfig; resolve non-interactively instead + yes "" | make oldconfig >/dev/null + for c in ${CFG_ON}; do + grep -q "^CONFIG_${c}=y$" .config || die "busybox: CONFIG_${c} did not stick" + done +} + +do_build() { + cd "${WRKSRC}" + make -j"$(nproc)" + have=$(./busybox --list) + for a in ${REQUIRE}; do + printf '%s\n' "${have}" | grep -qx "${a}" || die "busybox: required applet missing: ${a}" + done + for a in ${FORBID}; do + printf '%s\n' "${have}" | grep -qx "${a}" && die "busybox: forbidden applet present: ${a}" || true + done +} + +do_install() { + cd "${WRKSRC}" + make CONFIG_PREFIX="${DESTDIR}" install + # initrd-era fossil: the kernel runs /linuxrc only on classic initrd, and + # UNOS boots initramfs-style (/init + switch_root). Prune it so the rootfs + # carries no dead symlinks. + rm -f "${DESTDIR}/linuxrc" +} diff --git a/packages/glibc/files/post-install b/packages/glibc/files/post-install @@ -0,0 +1,4 @@ +#!/bin/sh +# glibc post-install: (re)build the loader cache in the target root. +# Runs on the target (apk executes control scripts chrooted at install). +/sbin/ldconfig diff --git a/packages/glibc/template b/packages/glibc/template @@ -0,0 +1,45 @@ +# Template file for 'glibc' +pkgname=glibc +version=2.44 +revision=1 +short_desc="UNOS C library - glibc" +maintainer="finwo <finwo@pm.me>" +license="LGPL-2.1-or-later" +homepage="https://www.gnu.org/software/libc/" +distfiles="https://ftp.gnu.org/gnu/glibc/glibc-2.44.tar.xz" +checksum=37f600f2bef3c5e8300147059568b2a2e40a7ad6ccc65ce942556d49429cc667 +# runtime needs from base-files: /etc/nsswitch.conf, /etc/ld.so.conf +depends="base-files" + +# Notes: +# - glibc forbids in-source builds; configure runs in a sibling build dir. +# - Upstream mandates python3 for the build (allowed: build-only, never +# shipped). No `make check` locally (slow/flaky outside CI); CI runs it. +# - --enable-kernel=5.4: floor for any plausible UNOS kernel (generic target +# and switch kernels are 6.x; ONIE's kernel is irrelevant). +# - Single fat package (runtime + headers + static libs); split if size +# ever matters. No debuginfo split either. +# - Locales: C.UTF-8 only. No tzdata (UTC everywhere). + +do_configure() { + export LC_ALL=C + mkdir -p "${WORK}/build" + cd "${WORK}/build" + "${WRKSRC}/configure" \ + --prefix=/usr \ + --enable-kernel=5.4 \ + --disable-werror +} + +do_build() { + cd "${WORK}/build" + make -j"$(nproc)" +} + +do_install() { + cd "${WORK}/build" + make install DESTDIR="${DESTDIR}" + # make install does not create the locale dir itself + mkdir -p "${DESTDIR}/usr/lib/locale" + ./locale/localedef --prefix="${DESTDIR}" -i C -f UTF-8 C.UTF-8 +}