commit 40626fe3a9ed50ec119acc41f0965462b4868120
parent 42df5f9ef26eb60af7e1db352f615087ecdfe9c5
Author: finwo <finwo@pm.me>
Date: Fri, 18 Sep 2026 17:12:36 +0200
More packages, moving towards self-installing
Diffstat:
23 files changed, 953 insertions(+), 13 deletions(-)
diff --git a/mk/README.md b/mk/README.md
@@ -5,9 +5,22 @@ The whole of `build/` is generated output. Only `mk/` is source.
## Files
-- `build.sh <pkg>` -- the pipeline: source template -> fetch+verify ->
- extract -> patches -> `do_build`/`do_install` into `DESTDIR` -> emit signed
- `.apk` into `build/repo/<arch>/`.
+- `bootstrap-host.sh [--check|--force]` -- builds the host tools in
+ `build/host/bin` from pinned, checksummed tarballs: `muon` (bootstrapped from
+ a single amalgamated C file, so the chain terminates at a C compiler) and
+ `apk`. `--check` verifies presence *and version* and is the gate CI should
+ run first. Everything else assumes these exist.
+- `deps.sh {list|vars|graph|order|check}` -- the build-dependency graph, read
+ from each template's `makedepends`. `order` is a topological sort; `check`
+ validates it (dangling deps, self-deps, cycles, missing hostmakedepends).
+- `build-all.sh [--dry-run] [--skip-built] [pkg...]` -- build everything, or
+ one package and its closure, in `deps.sh order`.
+- `build.sh <pkg>` -- the pipeline: source template -> check
+ `hostmakedepends`/`makedepends` -> fetch+verify -> extract -> patches ->
+ `do_build`/`do_install` into `DESTDIR` -> emit signed `.apk` into
+ `build/repo/<arch>/`. Dependency checks run before any fetching, so a missing
+ host tool is named up front rather than surfacing 200 lines into someone
+ else's generated parser.
- `repo-index.sh [arch]` -- rebuild `APKINDEX.tar.gz` with host `apk index`,
then sign it in place (abuild-sign layout: gzipped sig tar record
prepended, RSA/SHA1 over the index bytes).
@@ -84,9 +97,27 @@ index. Current keys (dev estate, rotate before any release):
## 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`.
+sh, cc, curl or wget, tar, gzip, openssl, sha256sum, install, patch.
+`unshare` (user namespaces) for `chroot.sh` and `rootfs.sh`.
+apk-tools and muon are **not** host prerequisites -- `bootstrap-host.sh` builds
+them into `build/host/bin` from pinned sources.
+
+Per-package host tools (bison, flex, bc, perl, python3, ...) are declared as
+`hostmakedepends` in the templates that need them and checked by `build.sh`, so
+a CI image can be validated against the real requirement set with:
+
+```sh
+./mk/deps.sh check
+```
+
+A clean build from nothing is therefore:
+
+```sh
+./mk/bootstrap-host.sh # muon + apk into build/host/bin
+./mk/build-all.sh # every package, in dependency order
+./mk/repo-index.sh # signed APKINDEX
+./mk/rootfs.sh --force # assemble rootfs/
+```
## Rootfs (`rootfs.sh`, `chroot.sh`)
@@ -109,11 +140,21 @@ 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.
+ -Dhelp=disabled -Ddocs=disabled`, rpath `$ORIGIN/../lib`):
+ `build/host/bin/apk`, used by `repo-index.sh` for index generation and by
+ `rootfs.sh` for assembly.
+
+There is deliberately **no `apk.static`**. An earlier revision built one and
+justified it here as "the future target binary - no libcrypto needed on the
+switch". That was already wrong: UNOS packages `openssl`, so `libcrypto.so.3`
+and `libssl.so.3` are present on the switch and the target `apk` links them
+dynamically. Its only consumer was `rootfs.sh`, which produces a byte-identical
+rootfs with the dynamic binary. Building it cost 10.6MB against 2.0MB and
+required `libcrypto.a`/`libssl.a`/`libz.a`/`libzstd.a` on the build host --
+static libraries that most CI containers do not ship, turning a cosmetic
+preference into a hard bootstrap failure. If a static apk is ever wanted for
+recovering a switch with a broken loader, it belongs in `packages/` as a target
+artifact.
Muon itself is pinned: v0.6.0,
`https://github.com/muon-build/muon/archive/refs/tags/0.6.0.tar.gz`,
@@ -127,3 +168,11 @@ 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.
+
+**This is no longer prose to follow by hand** -- `mk/bootstrap-host.sh` is the
+executable form of the above. `--check` verifies presence *and version* (a
+stale binary from an older pin is worse than a missing one, because everything
+downstream still appears to work), and `--force` rebuilds from the pinned
+tarballs. muon's `meson-docs`/`meson-tests` subprojects are disabled because
+its `setup` step otherwise git-clones them from GitHub mid-build, which has no
+place in a bootstrap driven by checksummed tarballs.
diff --git a/mk/bootstrap-host.sh b/mk/bootstrap-host.sh
@@ -0,0 +1,212 @@
+#!/bin/sh
+# mk/bootstrap-host.sh - build the host bootstrap tools into build/host/.
+#
+# ./mk/bootstrap-host.sh # build what is missing
+# ./mk/bootstrap-host.sh --force # rebuild everything
+# ./mk/bootstrap-host.sh --check # verify only, build nothing (CI gate)
+#
+# Produces:
+# build/host/bin/muon pinned muon, bootstrapped from C (no Python)
+# build/host/bin/apk dynamic apk v3, used by repo-index.sh/rootfs.sh
+
+#
+# WHY THIS EXISTS
+# ---------------
+# These three binaries are the input to every other build: mk/build.sh puts
+# build/host/bin on PATH, repo-index.sh generates APKINDEX with `apk index`,
+# and rootfs.sh installs with `apk add`. Until now they were built by hand
+# following prose in mk/README.md, which meant a fresh checkout or a CI
+# container could not reproduce the toolchain at all. This script is that
+# prose, executable.
+#
+# Python is banned from our tooling, so apk-tools is configured with muon
+# rather than meson; upstream supports this. muon itself bootstraps from a
+# single amalgamated C file, so the chain terminates at a C compiler.
+set -eu
+
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "${HERE}/.." && pwd)
+HOSTDIR="${ROOT}/build/host"
+SRCDEST="${ROOT}/build/work/sources"
+WORK="${ROOT}/build/work/hostbootstrap"
+
+# --- pins (keep in sync with mk/README.md "Host apk-tools v3") -------------
+MUON_VERSION=0.6.0
+MUON_URL="https://github.com/muon-build/muon/archive/refs/tags/${MUON_VERSION}.tar.gz"
+MUON_TARBALL="muon-${MUON_VERSION}.tar.gz"
+MUON_SHA256=5300e58c4b4d43e3026856004c79d746075aaa9d9e66d76ba9f32ce249495b81
+
+APK_VERSION=3.0.8
+APK_URL="https://gitlab.alpinelinux.org/alpine/apk-tools/-/archive/v${APK_VERSION}/apk-tools-v${APK_VERSION}.tar.gz"
+APK_TARBALL="apk-tools-v${APK_VERSION}.tar.gz"
+APK_SHA256=e81c64a6e7c3806d45d4622c253e757aee970be84048554fa0eed79310e9453e
+
+FORCE=0
+CHECK=0
+for a in "$@"; do
+ case "$a" in
+ --force) FORCE=1 ;;
+ --check) CHECK=1 ;;
+ -h|--help) sed -n '2,12p' "$0"; exit 0 ;;
+ *) echo "bootstrap-host.sh: unknown argument: $a" >&2; exit 1 ;;
+ esac
+done
+
+msg() { printf '==> %s\n' "$*"; }
+die() { printf 'bootstrap-host.sh: error: %s\n' "$*" >&2; exit 1; }
+
+# Run a build step quietly, but dump the whole log if it fails. Build noise is
+# useless until something breaks, at which point you want all of it.
+LOGFILE="${WORK}/bootstrap.log"
+run() {
+ # run <description> <cmd...>
+ what="$1"; shift
+ if ! "$@" >>"${LOGFILE}" 2>&1; then
+ echo "bootstrap-host.sh: ${what} failed; last 40 lines:" >&2
+ tail -40 "${LOGFILE}" >&2
+ exit 1
+ fi
+}
+
+fetch() {
+ # fetch <url> <tarball> <sha256>
+ f="${SRCDEST}/$2"
+ mkdir -p "${SRCDEST}"
+ if [ ! -f "${f}" ]; then
+ msg "fetching $2"
+ curl -fL -o "${f}" "$1" || wget -O "${f}" "$1" || die "could not fetch $1"
+ fi
+ echo "$3 ${f}" | sha256sum -c - >/dev/null || die "checksum mismatch: $2"
+}
+
+# --- verification ----------------------------------------------------------
+# Version-checked, not just existence-checked: a stale binary from an older pin
+# is worse than a missing one, because everything downstream still "works".
+check_tools() {
+ rc=0
+ if [ -x "${HOSTDIR}/bin/muon" ]; then
+ v=$("${HOSTDIR}/bin/muon" version 2>/dev/null | head -1 || true)
+ case "$v" in
+ *"${MUON_VERSION}"*) msg "muon ok (${v})" ;;
+ *) echo " muon present but not ${MUON_VERSION}: ${v:-unknown}" >&2; rc=1 ;;
+ esac
+ else
+ echo " muon missing" >&2; rc=1
+ fi
+
+ if [ -x "${HOSTDIR}/bin/apk" ]; then
+ v=$("${HOSTDIR}/bin/apk" --version 2>/dev/null | head -1 || true)
+ case "$v" in
+ *"${APK_VERSION}"*) msg "apk ok (${v})" ;;
+ *) echo " apk present but not ${APK_VERSION}: ${v:-unknown}" >&2; rc=1 ;;
+ esac
+ else
+ echo " apk missing" >&2; rc=1
+ fi
+ return $rc
+}
+
+if [ "${CHECK}" = "1" ]; then
+ if check_tools; then
+ msg "host tools present and pinned"
+ exit 0
+ fi
+ die "host tools missing or stale; run ./mk/bootstrap-host.sh"
+fi
+
+if [ "${FORCE}" != "1" ] && check_tools 2>/dev/null; then
+ msg "host tools already present (use --force to rebuild)"
+ exit 0
+fi
+
+command -v cc >/dev/null 2>&1 || command -v gcc >/dev/null 2>&1 \
+ || die "no C compiler on PATH"
+
+mkdir -p "${HOSTDIR}/bin" "${WORK}"
+rm -f "${LOGFILE}"
+
+# --- muon ------------------------------------------------------------------
+# bootstrap.sh compiles src/amalgam.c with a plain c99 compiler, giving a
+# muon capable of building the real muon. Pure C, no Python anywhere.
+fetch "${MUON_URL}" "${MUON_TARBALL}" "${MUON_SHA256}"
+
+msg "building muon ${MUON_VERSION}"
+rm -rf "${WORK}/muon"
+mkdir -p "${WORK}/muon"
+tar -xzf "${SRCDEST}/${MUON_TARBALL}" -C "${WORK}/muon"
+MUONSRC="${WORK}/muon/muon-${MUON_VERSION}"
+[ -d "${MUONSRC}" ] || die "unexpected muon tarball layout"
+
+# meson-docs and meson-tests are wrap subprojects that muon git-clones from
+# GitHub during setup. They are documentation and upstream's own test corpus;
+# we need neither, and an unpinned network fetch has no business in a bootstrap
+# that is otherwise driven entirely by pinned, checksummed tarballs. tracy is a
+# profiler. Disabling all three makes this step hermetic.
+MUON_OPTS="-Dmeson-docs=disabled -Dmeson-tests=disabled -Dtracy=disabled -Dui=disabled"
+
+cd "${MUONSRC}"
+run "muon bootstrap" sh bootstrap.sh build-boot
+[ -x build-boot/muon-bootstrap ] || die "muon bootstrap produced no binary"
+
+# Build the real muon with the bootstrap one. Samurai is vendored in muon, so
+# no ninja on the host is required either.
+# shellcheck disable=SC2086
+run "muon setup" ./build-boot/muon-bootstrap setup ${MUON_OPTS} build
+run "muon build" ./build-boot/muon-bootstrap -C build samu
+[ -x build/muon ] || die "muon self-build produced no binary"
+install -m 0755 build/muon "${HOSTDIR}/bin/muon"
+cd "${ROOT}"
+msg "muon -> ${HOSTDIR}/bin/muon"
+
+PATH="${HOSTDIR}/bin:${PATH}"
+export PATH
+
+# --- apk-tools -------------------------------------------------------------
+fetch "${APK_URL}" "${APK_TARBALL}" "${APK_SHA256}"
+
+rm -rf "${WORK}/apk"
+mkdir -p "${WORK}/apk"
+tar -xzf "${SRCDEST}/${APK_TARBALL}" -C "${WORK}/apk"
+APKSRC="${WORK}/apk/apk-tools-v${APK_VERSION}"
+[ -d "${APKSRC}" ] || APKSRC="${WORK}/apk/$(ls -A "${WORK}/apk" | head -1)"
+[ -d "${APKSRC}" ] || die "unexpected apk-tools tarball layout"
+
+# lua/python/docs off: lua is a binding we do not use, python is banned, and
+# docs need scdoc. help needs lua, so it goes too.
+APK_OPTS="-Dlua=disabled -Dpython=disabled -Dhelp=disabled -Ddocs=disabled"
+
+msg "building apk ${APK_VERSION} (dynamic)"
+cd "${APKSRC}"
+rm -rf build-dyn
+# RUNPATH $ORIGIN/../lib so the binary finds any libs we later place in
+# build/host/lib without needing LD_LIBRARY_PATH at every call site.
+# shellcheck disable=SC2086
+run "apk setup (dynamic)" muon setup \
+ -Dprefix="${HOSTDIR}" \
+ ${APK_OPTS} \
+ -Dc_link_args="-Wl,-rpath,\$ORIGIN/../lib" \
+ build-dyn
+run "apk build (dynamic)" muon -C build-dyn samu
+run "apk install (dynamic)" muon -C build-dyn install
+[ -x "${HOSTDIR}/bin/apk" ] || die "dynamic apk not installed"
+msg "apk -> ${HOSTDIR}/bin/apk"
+
+# NOTE: we deliberately do NOT build an apk.static.
+#
+# There used to be one, justified in mk/README.md as "the future target binary
+# - no libcrypto needed on the switch". That justification was already false:
+# UNOS packages openssl, so libcrypto.so.3/libssl.so.3 are on the switch and
+# the target apk links them dynamically like everything else. Its only real
+# consumer was mk/rootfs.sh, which works identically with the dynamic binary
+# (verified: same 24 packages, same rootfs).
+#
+# Building it cost 10.6MB versus 2.0MB and, worse, required libcrypto.a,
+# libssl.a, libz.a and libzstd.a on the build host -- static variants that many
+# distributions and most CI containers do not ship at all. That is a hard
+# bootstrap failure in exchange for nothing.
+#
+# If a statically linked apk is ever wanted for disaster recovery on a switch
+# with a broken loader, it belongs in packages/ as a target artifact, not here.
+
+check_tools || die "post-build verification failed"
+msg "host bootstrap complete"
diff --git a/mk/build-all.sh b/mk/build-all.sh
@@ -0,0 +1,64 @@
+#!/bin/sh
+# mk/build-all.sh - build packages in dependency order.
+#
+# ./mk/build-all.sh every package, correctly ordered
+# ./mk/build-all.sh gptfdisk that package and everything it needs
+# ./mk/build-all.sh --skip-built don't rebuild packages already in the repo
+# ./mk/build-all.sh --dry-run print the order, build nothing
+#
+# Order comes from mk/deps.sh, which derives it from each template's
+# `makedepends`. Before this existed, build order lived only in the head of
+# whoever was driving the build, which is precisely what a CI runner does not
+# have.
+set -eu
+
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "${HERE}/.." && pwd)
+
+DRY=0
+SKIP=0
+PKGS=""
+for a in "$@"; do
+ case "$a" in
+ --dry-run) DRY=1 ;;
+ --skip-built) SKIP=1 ;;
+ -h|--help) sed -n '2,10p' "$0"; exit 0 ;;
+ -*) echo "build-all.sh: unknown option: $a" >&2; exit 1 ;;
+ *) PKGS="${PKGS} $a" ;;
+ esac
+done
+
+msg() { printf '==> %s\n' "$*"; }
+die() { printf 'build-all.sh: error: %s\n' "$*" >&2; exit 1; }
+
+# Host tools are the root of the dependency graph: without them nothing builds.
+"${ROOT}/mk/bootstrap-host.sh" --check >/dev/null 2>&1 \
+ || die "host tools missing or stale; run ./mk/bootstrap-host.sh"
+
+# shellcheck disable=SC2086
+ORDER=$("${ROOT}/mk/deps.sh" order ${PKGS}) || die "could not resolve build order"
+
+if [ "${DRY}" = "1" ]; then
+ printf '%s\n' "${ORDER}"
+ exit 0
+fi
+
+total=$(printf '%s\n' "${ORDER}" | wc -l | tr -d ' ')
+n=0
+built=0
+skipped=0
+
+for p in ${ORDER}; do
+ n=$((n + 1))
+ if [ "${SKIP}" = "1" ] && ls "${ROOT}/build/repo/x86_64/${p}"-*.apk >/dev/null 2>&1; then
+ msg "[${n}/${total}] ${p}: already built, skipping"
+ skipped=$((skipped + 1))
+ continue
+ fi
+ msg "[${n}/${total}] ${p}"
+ "${ROOT}/mk/build.sh" "${p}" || die "${p} failed"
+ built=$((built + 1))
+done
+
+msg "built ${built}, skipped ${skipped}, of ${total}"
+msg "now run ./mk/repo-index.sh to refresh APKINDEX"
diff --git a/mk/build.sh b/mk/build.sh
@@ -27,6 +27,13 @@ homepage=
distfiles=
checksum=
depends=
+# makedepends: other UNOS packages whose headers/libs must be in build/sysroot
+# before this one compiles. Determines build order (see mk/deps.sh).
+# hostmakedepends: commands that must exist on the build host. Checked, never
+# built by us.
+# Both are declared in packages/README.md and were, until now, read by nothing.
+makedepends=
+hostmakedepends=
provides=
replaces=
subpackages=
@@ -71,6 +78,35 @@ ARCH="x86_64"
OUTDIR="${ROOT}/build/repo/${ARCH}"
mkdir -p "${SRCDEST}" "${WORK}" "${OUTDIR}" "${SYSROOT}"
+
+# Host-built tools (muon) must be on PATH before hostmakedepends is checked,
+# otherwise a package declaring `muon` fails the check despite muon being
+# present in build/host/bin. Exported again below with PKG_CONFIG_PATH; doing
+# it here as well is harmless and keeps the check honest.
+export PATH="${ROOT}/build/host/bin:${PATH}"
+
+# --- build dependencies ---------------------------------------------------
+# Checked before anything is fetched or compiled. A missing bison surfacing as
+# a syntax error 200 lines into someone else's generated parser is the kind of
+# failure that costs an afternoon; naming it up front costs nothing.
+for _h in ${hostmakedepends}; do
+ command -v "${_h}" >/dev/null 2>&1 \
+ || die "hostmakedepends: '${_h}' not found on PATH (required to build ${pkgname})"
+done
+
+for _m in ${makedepends}; do
+ [ -f "${ROOT}/packages/${_m}/template" ] \
+ || die "makedepends: '${_m}' is not a package under packages/"
+ # Presence of a built .apk is the signal that its sysroot seed has been
+ # installed; templates that other packages build against install twice
+ # (DESTDIR + SYSROOT), so one implies the other.
+ if ! ls "${OUTDIR}/${_m}"-*.apk >/dev/null 2>&1; then
+ die "makedepends '${_m}' has not been built.
+ Build it first: ./mk/build.sh ${_m}
+ Or build in order: ./mk/build-all.sh ${pkgname}"
+ fi
+done
+
# fresh staging every build: templates must be idempotent AND ghost-free
rm -rf "${DESTDIR}"
mkdir -p "${DESTDIR}"
diff --git a/mk/deps.sh b/mk/deps.sh
@@ -0,0 +1,176 @@
+#!/bin/sh
+# mk/deps.sh - the build-dependency graph.
+#
+# ./mk/deps.sh list every package name
+# ./mk/deps.sh vars <pkg> that package's declared dependencies
+# ./mk/deps.sh order [pkg...] topological build order (all if no args)
+# ./mk/deps.sh graph machine-readable "pkg<TAB>dep" edges
+# ./mk/deps.sh check validate the whole graph, exit 1 if bad
+#
+# WHY THIS EXISTS
+# ---------------
+# packages/README.md has always documented hostmakedepends/makedepends, but
+# nothing read them and no template set them, so build order lived only in
+# whoever's head was driving the build. That is fine until CI, a fresh
+# checkout, or a second person needs to build from nothing.
+#
+# Three kinds of dependency, deliberately distinct:
+#
+# depends runtime. Recorded in the .apk, resolved by apk on the
+# switch. Does NOT affect build order.
+# makedepends other UNOS packages whose headers/libs must be in
+# build/sysroot before this one compiles. This is what
+# determines build order.
+# hostmakedepends commands that must exist on the build host (bison,
+# flex, python3 for glibc...). Checked, never built.
+#
+# Templates are shell fragments, so reading them means sourcing them. That is
+# exactly what mk/build.sh does; here it happens in a subshell that only ever
+# reads variables, and the do_* functions are defined but never called.
+set -eu
+
+HERE=$(cd "$(dirname "$0")" && pwd)
+ROOT=$(cd "${HERE}/.." && pwd)
+PKGDIR="${ROOT}/packages"
+
+die() { printf 'deps.sh: error: %s\n' "$*" >&2; exit 1; }
+
+# Same PATH that mk/build.sh gives templates, so hostmakedepends like `muon`
+# resolve from build/host/bin rather than appearing absent.
+PATH="${ROOT}/build/host/bin:${PATH}"
+export PATH
+
+list_pkgs() {
+ for d in "${PKGDIR}"/*/; do
+ [ -f "${d}template" ] || continue
+ basename "${d}"
+ done | sort
+}
+
+# Print "depends|makedepends|hostmakedepends" for one package.
+# Runs in a subshell: template assignments cannot leak into the caller.
+read_vars() {
+ pkg="$1"
+ t="${PKGDIR}/${pkg}/template"
+ [ -f "${t}" ] || die "no such package: ${pkg}"
+ (
+ depends=
+ makedepends=
+ hostmakedepends=
+ subpackages=
+ # Templates reference these; define them so sourcing cannot fail on an
+ # unset variable under `set -u`.
+ SYSROOT=; DESTDIR=; WRKSRC=; FILESDIR=; WORK=; SRCDEST=; UNOS_MKDIR=
+ die() { :; }
+ msg() { :; }
+ # shellcheck disable=SC1090
+ . "${t}" >/dev/null 2>&1 || true
+ printf '%s|%s|%s\n' "${depends}" "${makedepends}" "${hostmakedepends}"
+ )
+}
+
+field() { read_vars "$1" | cut -d'|' -f"$2"; }
+
+cmd_vars() {
+ pkg="${1:?usage: deps.sh vars <pkg>}"
+ v=$(read_vars "${pkg}")
+ printf 'package: %s\n' "${pkg}"
+ printf 'depends: %s\n' "$(printf '%s' "${v}" | cut -d'|' -f1)"
+ printf 'makedepends: %s\n' "$(printf '%s' "${v}" | cut -d'|' -f2)"
+ printf 'hostmakedepends: %s\n' "$(printf '%s' "${v}" | cut -d'|' -f3)"
+}
+
+cmd_graph() {
+ for p in $(list_pkgs); do
+ for d in $(field "${p}" 2); do
+ printf '%s\t%s\n' "${p}" "${d}"
+ done
+ done
+}
+
+# Depth-first topological sort. State lives in a temp dir because POSIX sh has
+# no associative arrays: <tmp>/seen.<pkg> marks done, <tmp>/path.<pkg> marks
+# "currently on the recursion stack", which is how cycles are detected.
+TMP=
+cleanup() { [ -n "${TMP}" ] && rm -rf "${TMP}"; }
+trap cleanup EXIT
+
+# NOTE: everything here uses "$1" rather than a named variable. POSIX sh has no
+# function-local variables, so a recursive function that assigns `pkg="$1"`
+# has that assignment clobbered by its own recursive calls -- on return it
+# would mark and emit the last child instead of itself, producing duplicates
+# and silently dropping every package that has dependencies. Positional
+# parameters, by contrast, are saved and restored per invocation.
+visit() {
+ [ -e "${TMP}/seen.$1" ] && return 0
+ if [ -e "${TMP}/path.$1" ]; then
+ die "dependency cycle involving '$1'"
+ fi
+ [ -f "${PKGDIR}/$1/template" ] || die "'$1' is a makedepends of something but has no template"
+ : > "${TMP}/path.$1"
+ for d in $(field "$1" 2); do
+ visit "${d}"
+ done
+ rm -f "${TMP}/path.$1"
+ : > "${TMP}/seen.$1"
+ echo "$1" >> "${TMP}/order"
+}
+
+cmd_order() {
+ TMP=$(mktemp -d /tmp/unos-deps.XXXXXX)
+ : > "${TMP}/order"
+ if [ $# -gt 0 ]; then
+ for p in "$@"; do visit "${p}"; done
+ else
+ for p in $(list_pkgs); do visit "${p}"; done
+ fi
+ cat "${TMP}/order"
+}
+
+cmd_check() {
+ rc=0
+ TMP=$(mktemp -d /tmp/unos-deps.XXXXXX)
+ : > "${TMP}/order"
+
+ for p in $(list_pkgs); do
+ # every makedepends must name a real package
+ for d in $(field "${p}" 2); do
+ if [ ! -f "${PKGDIR}/${d}/template" ]; then
+ echo "FAIL: ${p}: makedepends '${d}' is not a package" >&2
+ rc=1
+ fi
+ if [ "${d}" = "${p}" ]; then
+ echo "FAIL: ${p}: depends on itself" >&2
+ rc=1
+ fi
+ done
+ # hostmakedepends must be resolvable on this host
+ for h in $(field "${p}" 3); do
+ command -v "${h}" >/dev/null 2>&1 || {
+ echo "FAIL: ${p}: hostmakedepends '${h}' not found on PATH" >&2
+ rc=1
+ }
+ done
+ done
+
+ # cycle detection, via the same DFS the ordering uses
+ if [ "${rc}" = "0" ]; then
+ for p in $(list_pkgs); do visit "${p}"; done >/dev/null 2>&1 || {
+ echo "FAIL: dependency graph has a cycle" >&2
+ for p in $(list_pkgs); do visit "${p}" >/dev/null; done
+ rc=1
+ }
+ fi
+
+ [ "${rc}" = "0" ] && echo "deps.sh: graph OK ($(list_pkgs | wc -l | tr -d ' ') packages)"
+ return $rc
+}
+
+case "${1:-}" in
+ list) list_pkgs ;;
+ vars) shift; cmd_vars "$@" ;;
+ graph) cmd_graph ;;
+ order) shift; cmd_order "$@" ;;
+ check) cmd_check ;;
+ *) sed -n '2,10p' "$0"; exit 1 ;;
+esac
diff --git a/mk/rootfs.sh b/mk/rootfs.sh
@@ -13,7 +13,7 @@ set -eu
HERE=$(cd "$(dirname "$0")" && pwd)
ROOT=$(cd "${HERE}/.." && pwd)
ROOTFS="${ROOT}/rootfs"
-APK="${ROOT}/build/host/bin/apk.static"
+APK="${ROOT}/build/host/bin/apk"
CACHE="${ROOT}/build/work/apk-cache"
REPO="file://${ROOT}/build/repo"
@@ -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
+ 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
# Deterministic loader cache: package post-installs refresh it during the
# transaction, but assembly must not depend on script-execution order.
diff --git a/packages/README.md b/packages/README.md
@@ -35,6 +35,15 @@ directory is ever built by hand on a target switch.
## Rules
+- **Declare build dependencies.** `makedepends` lists other UNOS packages whose
+ headers/libs must be in `build/sysroot` before this one compiles;
+ `hostmakedepends` lists commands that must exist on the build host. Both are
+ enforced by `mk/build.sh` before anything is fetched, and `makedepends` is
+ what `mk/deps.sh` uses to compute build order. Keep them accurate in both
+ directions: a missing entry breaks a clean-checkout build, and a spurious one
+ fails a build that would otherwise work. `depends` is runtime only and does
+ **not** affect build order, so a package needed at both times appears twice
+ (see `gptfdisk`).
- **Pin everything.** Explicit versions and recorded checksums. No moving
targets, no floating branches.
- **Never vendor upstream source into this repository.** Sources are fetched at
diff --git a/packages/apk-tools/template b/packages/apk-tools/template
@@ -9,6 +9,8 @@ homepage="https://gitlab.alpinelinux.org/alpine/apk-tools"
distfiles="https://gitlab.alpinelinux.org/alpine/apk-tools/-/archive/v3.0.8/apk-tools-v3.0.8.tar.gz"
checksum=e81c64a6e7c3806d45d4622c253e757aee970be84048554fa0eed79310e9453e
depends="zlib openssl"
+makedepends="zlib openssl"
+hostmakedepends="muon"
# Notes:
# - Same version the host manager was built from (v3.0.8); target and host
diff --git a/packages/e2fsprogs/files/post-install b/packages/e2fsprogs/files/post-install
@@ -0,0 +1,6 @@
+#!/bin/sh
+# e2fsprogs post-install: refresh the loader cache so the new .so 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/e2fsprogs/template b/packages/e2fsprogs/template
@@ -0,0 +1,109 @@
+# Template file for 'e2fsprogs'
+pkgname=e2fsprogs
+version=1.47.4
+revision=0
+short_desc="ext2/3/4 filesystem utilities and libraries"
+maintainer="finwo <finwo@pm.me>"
+license="GPL-2.0-only, LGPL-2.0-only, BSD-3-Clause, MIT"
+homepage="https://e2fsprogs.sourceforge.net/"
+distfiles="https://www.kernel.org/pub/linux/kernel/people/tytso/e2fsprogs/v${version}/e2fsprogs-${version}.tar.xz"
+checksum=fd5bf388cbdbe006a3d3b318d983b2948382440acc85a87f1e7d108653e8db0b
+depends="glibc busybox"
+
+# Notes:
+# - Why this exists: without e2fsck a switch cannot repair its own root after
+# an unclean shutdown, which is not a recoverable position. mke2fs is also
+# what installer/install.sh formats the root partition with, so shipping this
+# is what lets a UNOS image install UNOS instead of borrowing host binaries.
+# - libuuid/libblkid: upstream defaults these OFF, expecting util-linux to
+# provide them. We do not package util-linux, and gptfdisk needs libuuid, so
+# we build e2fsprogs' own copies and they become the canonical providers.
+# - --with-root-prefix=/usr keeps everything under /usr/{sbin,lib} rather than
+# the historical /sbin + /lib split, matching glibc's --libdir=/usr/lib.
+# - Busybox already provides a cut-down blkid/findfs; those applets stay as
+# they are. The real blkid here ships as a library, not as a second binary,
+# to avoid an apk file conflict (apk refuses cross-package overwrites).
+
+do_configure() {
+ cd "${WRKSRC}"
+ # --sysconfdir=/etc is load-bearing: with a bare --prefix=/usr, autotools
+ # puts mke2fs.conf in /usr/etc, but mke2fs only ever reads /etc/mke2fs.conf.
+ # It would silently fall back to built-in defaults and ignore our config.
+ ./configure \
+ --prefix=/usr \
+ --with-root-prefix=/usr \
+ --sysconfdir=/etc \
+ --enable-elf-shlibs \
+ --disable-static \
+ --enable-libuuid \
+ --enable-libblkid \
+ --disable-uuidd \
+ --disable-fsck \
+ --disable-debugfs \
+ --disable-nls \
+ --disable-rpath \
+ --disable-testio-debug
+}
+
+do_build() {
+ cd "${WRKSRC}"
+ make -j"$(nproc)"
+}
+
+do_install() {
+ cd "${WRKSRC}"
+ make install DESTDIR="${DESTDIR}"
+ make install-libs DESTDIR="${DESTDIR}"
+
+ # Sysroot seed: gptfdisk and smartmontools build against libuuid.
+ make install DESTDIR="${SYSROOT}"
+ make install-libs DESTDIR="${SYSROOT}"
+
+ # Shared-only (packages/README.md): no .a, no libtool turds.
+ rm -f "${DESTDIR}"/usr/lib/*.a "${DESTDIR}"/usr/lib/*.la
+ rm -f "${SYSROOT}"/usr/lib/*.a "${SYSROOT}"/usr/lib/*.la
+
+ # `blkid` and `findfs` would collide with the busybox applets of the same
+ # name; apk refuses cross-package file overwrites, so drop ours.
+ rm -f "${DESTDIR}"/usr/sbin/blkid "${DESTDIR}"/usr/sbin/findfs
+ rm -f "${DESTDIR}"/usr/share/man/man8/blkid.8 \
+ "${DESTDIR}"/usr/share/man/man8/findfs.8
+
+ # No docs/locale in the image.
+ rm -rf "${DESTDIR}/usr/share/info" "${DESTDIR}/usr/share/locale"
+
+ # e2scrub drives online fsck of LVM snapshots via cron and udev. UNOS has
+ # none of those three, so the scripts could only ever fail confusingly.
+ rm -f "${DESTDIR}"/usr/sbin/e2scrub "${DESTDIR}"/usr/sbin/e2scrub_all
+ rm -f "${DESTDIR}"/etc/e2scrub.conf
+ rm -rf "${DESTDIR}"/etc/cron.d "${DESTDIR}"/usr/libexec
+ rm -f "${DESTDIR}"/usr/share/man/man8/e2scrub.8 \
+ "${DESTDIR}"/usr/share/man/man8/e2scrub_all.8 \
+ "${DESTDIR}"/usr/share/man/man5/e2scrub.conf.5
+ # udev rules: no udev (devtmpfs + mdev territory), and upstream hardcodes
+ # a lib64 path that does not match our /usr/lib layout either way.
+ rm -rf "${DESTDIR}"/usr/lib64 "${DESTDIR}"/usr/lib/udev
+ # initrd helper: UNOS boots without an initrd by design (see grub.cfg.in).
+ rm -f "${DESTDIR}"/usr/lib/e2initrd_helper
+
+ # compile_et/mk_cmds are build-time code generators for downstream
+ # consumers; they belong in the sysroot, not on a switch.
+ rm -f "${DESTDIR}"/usr/bin/compile_et "${DESTDIR}"/usr/bin/mk_cmds
+ rm -rf "${DESTDIR}"/usr/share/et "${DESTDIR}"/usr/share/ss
+ rm -f "${DESTDIR}"/usr/share/man/man1/compile_et.1 \
+ "${DESTDIR}"/usr/share/man/man1/mk_cmds.1
+
+ # The whole point of the package: assert the tools the installer needs.
+ for t in mke2fs e2fsck resize2fs tune2fs dumpe2fs; do
+ [ -x "${DESTDIR}/usr/sbin/${t}" ] || die "e2fsprogs: missing ${t}"
+ done
+ # mkfs.ext4 is a symlink to mke2fs; installer/install.sh calls it by name.
+ [ -e "${DESTDIR}/usr/sbin/mkfs.ext4" ] || die "e2fsprogs: no mkfs.ext4"
+ [ -e "${DESTDIR}/usr/sbin/fsck.ext4" ] || die "e2fsprogs: no fsck.ext4"
+ [ -f "${SYSROOT}/usr/include/uuid/uuid.h" ] || die "e2fsprogs: no sysroot uuid.h"
+ [ -f "${SYSROOT}/usr/lib/pkgconfig/uuid.pc" ] || die "e2fsprogs: no sysroot uuid.pc"
+ # mke2fs silently uses built-in defaults if this is not where it looks.
+ [ -f "${DESTDIR}/etc/mke2fs.conf" ] || die "e2fsprogs: mke2fs.conf not in /etc"
+ [ ! -d "${DESTDIR}/usr/etc" ] || die "e2fsprogs: stray /usr/etc"
+ [ ! -d "${DESTDIR}/usr/lib64" ] || die "e2fsprogs: stray /usr/lib64"
+}
diff --git a/packages/efibootmgr/template b/packages/efibootmgr/template
@@ -10,6 +10,7 @@ distfiles="https://github.com/rhboot/efibootmgr/archive/refs/tags/18.tar.gz"
checksum=442867d12f8525034a404fc8af3036dba8e1fc970998af2486c3b940dfad0874
# runtime links (readelf-proven at build): libefivar + libpopt.
depends="efivar popt"
+makedepends="efivar popt"
# Notes:
# - No configure step; lowercase make vars (prefix/libdir/sbindir) plus
diff --git a/packages/glibc/template b/packages/glibc/template
@@ -11,6 +11,7 @@ checksum=37f600f2bef3c5e8300147059568b2a2e40a7ad6ccc65ce942556d49429cc667
# runtime needs from base-files: /etc/nsswitch.conf, /etc/ld.so.conf.
# busybox first: the post-install runs under /bin/sh (see libmnl).
depends="base-files busybox"
+hostmakedepends="python3 bison gawk sed make"
# Notes:
# - glibc forbids in-source builds; configure runs in a sibling build dir.
diff --git a/packages/gptfdisk/template b/packages/gptfdisk/template
@@ -0,0 +1,52 @@
+# Template file for 'gptfdisk'
+pkgname=gptfdisk
+version=1.0.10
+revision=0
+short_desc="GPT partitioning tools (sgdisk)"
+maintainer="finwo <finwo@pm.me>"
+license="GPL-2.0-only"
+homepage="https://www.rodsbooks.com/gdisk/"
+distfiles="https://downloads.sourceforge.net/project/gptfdisk/gptfdisk/${version}/gptfdisk-${version}.tar.gz"
+checksum=2abed61bc6d2b9ec498973c0440b8b804b7a72d7144069b5a9209b2ad693a282
+# libstdc++ is a real runtime dep: this is the first C++ package in UNOS.
+depends="glibc busybox libstdc++ e2fsprogs popt"
+makedepends="e2fsprogs popt libstdc++"
+
+# Notes:
+# - Why: installer/install.sh partitions with sgdisk. ONIE provides it on a
+# switch, but without it here a UNOS image cannot install UNOS -- which is
+# what makes the plain qemu image usable as a rescue/installer environment.
+# - C++: linked against our packaged libstdc++ (see that template for why we
+# build the runtime from GCC source rather than static-linking it).
+# - libuuid comes from e2fsprogs (upstream e2fsprogs defaults it off; our
+# template enables it precisely because this package needs it).
+# - Upstream ships a plain Makefile, no configure. It also builds gdisk and
+# cgdisk; cgdisk needs ncurses, which UNOS does not package, so only the
+# non-curses tools are built and only sgdisk is shipped.
+
+do_build() {
+ cd "${WRKSRC}"
+ # Build against the sysroot so we link our glibc/libuuid/libstdc++ rather
+ # than the host's. gdisk/sgdisk/fixparts only; cgdisk needs ncurses.
+ make -j"$(nproc)" \
+ CXX="g++" \
+ CXXFLAGS="-O2 -D_FILE_OFFSET_BITS=64 -I${SYSROOT}/usr/include" \
+ LDFLAGS="-L${SYSROOT}/usr/lib -Wl,-rpath-link,${SYSROOT}/usr/lib" \
+ sgdisk
+}
+
+do_install() {
+ cd "${WRKSRC}"
+ vmkdir usr/sbin
+ vinstall sgdisk 0755 usr/sbin
+ vmkdir usr/share/man/man8
+ [ -f sgdisk.8 ] && vinstall sgdisk.8 0644 usr/share/man/man8
+
+ [ -x "${DESTDIR}/usr/sbin/sgdisk" ] || die "gptfdisk: no sgdisk"
+ # Assert we picked up our own C++ runtime, not a static copy and not the
+ # host's: a static libstdc++ would silently violate the shared-only rule.
+ readelf -d "${DESTDIR}/usr/sbin/sgdisk" | grep -q 'libstdc++\.so\.6' \
+ || die "gptfdisk: sgdisk does not link libstdc++.so.6 (static C++ runtime?)"
+ readelf -d "${DESTDIR}/usr/sbin/sgdisk" | grep -q 'libuuid\.so\.1' \
+ || die "gptfdisk: sgdisk does not link libuuid.so.1"
+}
diff --git a/packages/grub/template b/packages/grub/template
@@ -11,6 +11,8 @@ distfiles="https://ftp.gnu.org/gnu/grub/grub-2.14.tar.xz"
checksum=bc8d3c73535b8838d8c8e2654d73edc4e6ae8c8acdb45d5df5dc9a1547446d43
# xz: grub-install links liblzma (readelf-proven, not guessed).
depends="xz"
+makedepends="xz"
+hostmakedepends="bison flex"
# Notes:
# - x86_64-efi ONLY in v1 (not i386-pc): grub 2.14's pc kernel.img links
diff --git a/packages/iproute2/template b/packages/iproute2/template
@@ -9,6 +9,8 @@ homepage="https://www.kernel.org/pub/linux/utils/net/iproute2/"
distfiles="https://www.kernel.org/pub/linux/utils/net/iproute2/iproute2-6.19.0.tar.xz"
checksum=9332213d35480b647086a70c302de8568de83455a98774d35de216c4ce191006
depends="libmnl libcap"
+makedepends="libmnl libcap"
+hostmakedepends="bison flex"
# Notes:
# - `ip` + `rtmon` + `ss` ONLY (PLAN section 10): the top-level
diff --git a/packages/libstdc++/files/post-install b/packages/libstdc++/files/post-install
@@ -0,0 +1,6 @@
+#!/bin/sh
+# libstdc++ post-install: refresh the loader cache so the new .so 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/libstdc++/template b/packages/libstdc++/template
@@ -0,0 +1,143 @@
+# Template file for 'libstdc++'
+pkgname=libstdc++
+version=14.2.0
+revision=0
+short_desc="GNU Standard C++ library and GCC unwinder runtime"
+maintainer="finwo <finwo@pm.me>"
+license="GPL-3.0-or-later WITH GCC-exception-3.1"
+homepage="https://gcc.gnu.org/"
+# GCC plus its pinned in-tree prerequisites. Only the first distfile is
+# extracted by the driver; do_configure places the rest (see below).
+# Prerequisite versions are GCC 14.2.0's own pins, taken verbatim from
+# contrib/download_prerequisites -- not chosen by us.
+distfiles="https://ftp.gnu.org/gnu/gcc/gcc-${version}/gcc-${version}.tar.xz
+ https://gcc.gnu.org/pub/gcc/infrastructure/gmp-6.2.1.tar.bz2
+ https://gcc.gnu.org/pub/gcc/infrastructure/mpfr-4.1.0.tar.bz2
+ https://gcc.gnu.org/pub/gcc/infrastructure/mpc-1.2.1.tar.gz"
+checksum="a7b39bc69cbf9e25826c5a60ab26477001f7c08d85cec04bc0e29cabed6f3cc9
+ eae9326beb4158c386e39a356818031bd28f3124cf915f8c5b1dc4c7a36b4d7c
+ feced2d430dd5a97805fa289fed3fc8ff2b094c02d05287fd6133e7f1f0ec926
+ 17503d2c395dfcf106b622dc142683c1199431d095367c6aacba6eec30340459"
+depends="glibc busybox"
+
+# Notes:
+# - Why: sgdisk (gptfdisk) and smartctl (smartmontools) are C++, and UNOS
+# shipped no C++ runtime at all. Static-linking the runtime into each binary
+# was the alternative; packaging it keeps packages/README.md's shared-only
+# rule intact and amortises across any future C++ package.
+# - We build GCC, but we ship only libstdc++.so and libgcc_s.so. No compiler,
+# no headers, no libsupc++.a. GCC is a means to the runtime here, nothing
+# more; do_install takes exactly two .so trees out of the tree.
+# - ABI: host g++ 14.2.1 emits at most GLIBCXX_3.4.33, which GCC 14.2.0's
+# libstdc++ provides. Bumping this package below the host compiler version
+# would break C++ packages at link time -- the assert in do_install guards it.
+# - --disable-bootstrap: a 3-stage bootstrap builds the compiler three times to
+# prove it can rebuild itself. We are not shipping the compiler, so stage1 is
+# sufficient and roughly a third of the build time.
+# - gmp/mpfr/mpc are build-time only and never packaged. GCC builds them
+# in-tree when their source dirs are present, which is the upstream-supported
+# way and avoids three packages we would otherwise never install.
+
+do_configure() {
+ cd "${WRKSRC}"
+
+ # In-tree prerequisites: GCC's build system picks these up automatically
+ # when unpacked under the source root with their version suffix stripped.
+ for dep in gmp-6.2.1 mpfr-4.1.0 mpc-1.2.1; do
+ name=${dep%%-*}
+ [ -d "${name}" ] && continue
+ case "${dep}" in
+ *.gz|mpc-*) tar -xf "${SRCDEST}/${dep}.tar.gz" -C . ;;
+ *) tar -xf "${SRCDEST}/${dep}.tar.bz2" -C . ;;
+ esac
+ mv "${dep}" "${name}" || die "libstdc++: could not stage ${name}"
+ done
+ for name in gmp mpfr mpc; do
+ [ -d "${name}" ] || die "libstdc++: missing in-tree ${name}"
+ done
+
+ # Target libraries ignore --libdir: their location comes from the multilib
+ # osdir, which on x86_64 defaults to ../lib64 even with --disable-multilib.
+ # UNOS is /usr/lib throughout (glibc is built --libdir=/usr/lib), so
+ # retarget m64 the way distros do rather than shipping a stray /usr/lib64.
+ sed -i 's|m64=../lib64|m64=../lib|' gcc/config/i386/t-linux64 \
+ || die "libstdc++: could not retarget MULTILIB_OSDIRNAMES"
+ grep -q 'm64=../lib$' gcc/config/i386/t-linux64 \
+ || grep -q 'm64=../lib[^6]' gcc/config/i386/t-linux64 \
+ || die "libstdc++: MULTILIB_OSDIRNAMES retarget did not apply"
+
+ # GCC refuses to configure in its own source directory.
+ mkdir -p "${WORK}/gccbuild"
+ cd "${WORK}/gccbuild"
+ "${WRKSRC}/configure" \
+ --prefix=/usr \
+ --libdir=/usr/lib \
+ --disable-bootstrap \
+ --enable-languages=c++ \
+ --enable-shared \
+ --enable-threads=posix \
+ --enable-__cxa_atexit \
+ --enable-clocale=gnu \
+ --disable-multilib \
+ --disable-nls \
+ --disable-libstdcxx-pch \
+ --disable-werror \
+ --disable-libsanitizer \
+ --disable-libssp \
+ --disable-libquadmath \
+ --disable-libvtv
+}
+
+do_build() {
+ cd "${WORK}/gccbuild"
+ # Only the two target runtimes. Building `all` would produce a full
+ # compiler we then throw away.
+ make -j"$(nproc)" all-target-libgcc
+ make -j"$(nproc)" all-target-libstdc++-v3
+}
+
+do_install() {
+ cd "${WORK}/gccbuild"
+ make install-target-libgcc DESTDIR="${DESTDIR}"
+ make install-target-libstdc++-v3 DESTDIR="${DESTDIR}"
+
+ # Sysroot seed so gptfdisk/smartmontools link against our runtime.
+ make install-target-libgcc DESTDIR="${SYSROOT}"
+ make install-target-libstdc++-v3 DESTDIR="${SYSROOT}"
+
+ # Ship the runtime only. Everything below is compiler-side: static
+ # archives, linker scripts, headers and GCC's private lib tree.
+ rm -rf "${DESTDIR}/usr/lib/gcc" "${DESTDIR}/usr/include"
+ rm -rf "${DESTDIR}/usr/share"
+ rm -f "${DESTDIR}"/usr/lib/*.a "${DESTDIR}"/usr/lib/*.la
+ rm -f "${DESTDIR}"/usr/lib/*.py
+ # libstdc++ ships GDB pretty-printers as .py; Python is banned in the
+ # rootfs and they are debugger-side anyway.
+ find "${DESTDIR}" -name '*-gdb.py' -delete 2>/dev/null || true
+
+ # GCC builds target libs with -g; unstripped libstdc++.so is ~20MB against
+ # ~2.5MB stripped, which is not a reasonable thing to put on a switch.
+ # --strip-debug (not --strip-unneeded) is deliberate: it drops debug info
+ # only and cannot touch the dynamic symbol table the loader resolves.
+ for so in "${DESTDIR}"/usr/lib/*.so.*; do
+ [ -f "${so}" ] || continue
+ strip --strip-debug "${so}" || die "libstdc++: strip failed on ${so}"
+ done
+
+ [ -e "${DESTDIR}/usr/lib/libstdc++.so.6" ] || die "libstdc++: no libstdc++.so.6"
+ [ -e "${DESTDIR}/usr/lib/libgcc_s.so.1" ] || die "libstdc++: no libgcc_s.so.1"
+ # Catch a silent regression of the MULTILIB_OSDIRNAMES retarget above.
+ [ ! -d "${DESTDIR}/usr/lib64" ] || die "libstdc++: stray /usr/lib64 (multilib osdir not retargeted)"
+
+ # ABI floor: our runtime must be at least what the host compiler emits,
+ # or every C++ binary we build will fail to resolve symbols on the switch.
+ have=$(strings "${DESTDIR}/usr/lib/libstdc++.so.6"* 2>/dev/null \
+ | grep -oE 'GLIBCXX_3\.4\.[0-9]+' | sort -V | tail -1)
+ need=$(strings /usr/lib/libstdc++.so.6 2>/dev/null \
+ | grep -oE 'GLIBCXX_3\.4\.[0-9]+' | sort -V | tail -1)
+ [ -n "${have}" ] || die "libstdc++: could not read our GLIBCXX versions"
+ if [ -n "${need}" ] && [ "$(printf '%s\n%s\n' "${need}" "${have}" | sort -V | tail -1)" != "${have}" ]; then
+ die "libstdc++: ours (${have}) is older than the host compiler needs (${need})"
+ fi
+ msg "libstdc++: ABI ${have} (host compiler needs ${need:-unknown})"
+}
diff --git a/packages/linux-longterm/template b/packages/linux-longterm/template
@@ -19,6 +19,7 @@ checksum=ba2f60f858bf4d1f929101faa356c93dc8b925b17aaa9f95eabd4627758df613
# the meta points at this line's kernel build (exact pin incl. revision -
# apk's `=` only matches the full pkgver, so a bare version never matches)
depends="linux=${version}-r${revision}"
+hostmakedepends="bc flex bison"
subpackages="linux-longterm-headers linux linux-headers"
short_desc_linux_longterm_headers="UNOS kernel headers meta - longterm line"
diff --git a/packages/linux-mainline/template b/packages/linux-mainline/template
@@ -24,6 +24,7 @@ checksum=49b24119d7b92da75bba0daf5da4f7735c8cb948757a282e849092dcc681d8dd
# the meta points at this line's kernel build (exact pin incl. revision -
# apk's `=` only matches the full pkgver, so a bare version never matches)
depends="linux=${version}-r${revision}"
+hostmakedepends="bc flex bison"
subpackages="linux-mainline-headers linux linux-headers"
short_desc_linux_mainline_headers="UNOS kernel headers meta - mainline snapshot"
diff --git a/packages/linux-stable/template b/packages/linux-stable/template
@@ -19,6 +19,7 @@ checksum=55ddf0df8325d9dad96fcff7bd93977d22e3f50af06527572af59b77c7632b78
# the meta points at this line's kernel build (exact pin incl. revision -
# apk's `=` only matches the full pkgver, so a bare version never matches)
depends="linux=${version}-r${revision}"
+hostmakedepends="bc flex bison"
subpackages="linux-stable-headers linux linux-headers"
short_desc_linux_stable_headers="UNOS kernel headers meta - stable line"
diff --git a/packages/openssl/template b/packages/openssl/template
@@ -13,6 +13,7 @@ checksum=a8f84a39918ec6415ce765d9b429d313ba97b8143169c172e734b9514464f5b2
# exist before the script executes (ordering, not just runtime truth -
# apk runs each package's scripts right after unpacking it).
depends="glibc busybox"
+hostmakedepends="perl"
# Notes:
# - 3.5 is the LTS line (supported to 2030); the 3.6 feature line is
diff --git a/packages/smartmontools/template b/packages/smartmontools/template
@@ -0,0 +1,65 @@
+# Template file for 'smartmontools'
+pkgname=smartmontools
+version=7.5
+revision=0
+short_desc="S.M.A.R.T. disk health monitoring (smartctl)"
+maintainer="finwo <finwo@pm.me>"
+license="GPL-2.0-or-later"
+homepage="https://www.smartmontools.org/"
+distfiles="https://downloads.sourceforge.net/project/smartmontools/smartmontools/${version}/smartmontools-${version}.tar.gz"
+checksum=690b83ca331378da9ea0d9d61008c4b22dde391387b9bbad7f29387f2595f76e
+depends="glibc busybox libstdc++"
+makedepends="libstdc++"
+
+# Notes:
+# - Why: a switch's boot media is usually an eMMC or a small SSD, and its wear
+# state is the thing that predicts the next outage. smartctl is how you read
+# it. The S5248F-ON captured in PROGRESS.md 2.1 boots from /dev/sda.
+# - smartd (the polling daemon) is deliberately NOT shipped: it wants cron-like
+# scheduling and a mail transport, neither of which UNOS has. Health polling
+# belongs in the control plane later, not in a second supervisor.
+# - The drive database is built in (--with-nvme-devicescan aside, no runtime
+# update path). update-smart-drivedb needs curl/gpg and is not shipped.
+
+do_configure() {
+ cd "${WRKSRC}"
+ ./configure \
+ --prefix=/usr \
+ --sbindir=/usr/sbin \
+ --sysconfdir=/etc \
+ --disable-dependency-tracking \
+ --without-libcap-ng \
+ --without-libsystemd \
+ --without-selinux \
+ --without-gnupg \
+ --without-update-smart-drivedb \
+ --with-nvme-devicescan
+}
+
+do_build() {
+ cd "${WRKSRC}"
+ make -j"$(nproc)"
+}
+
+do_install() {
+ cd "${WRKSRC}"
+ make install DESTDIR="${DESTDIR}"
+
+ # smartd is not shipped (see notes): drop the daemon, its config, its
+ # init/service glue and its manpages rather than leaving dead weight.
+ rm -f "${DESTDIR}"/usr/sbin/smartd
+ rm -f "${DESTDIR}"/etc/smartd.conf
+ rm -f "${DESTDIR}"/usr/share/man/man8/smartd.8 \
+ "${DESTDIR}"/usr/share/man/man5/smartd.conf.5
+ rm -rf "${DESTDIR}"/etc/smartd_warning.d \
+ "${DESTDIR}"/usr/share/smartmontools \
+ "${DESTDIR}"/usr/libexec \
+ "${DESTDIR}"/lib/systemd "${DESTDIR}"/usr/lib/systemd \
+ "${DESTDIR}"/etc/init.d "${DESTDIR}"/etc/rc.d
+ rm -rf "${DESTDIR}/usr/share/doc" "${DESTDIR}/usr/share/locale"
+
+ [ -x "${DESTDIR}/usr/sbin/smartctl" ] || die "smartmontools: no smartctl"
+ [ ! -e "${DESTDIR}/usr/sbin/smartd" ] || die "smartmontools: smartd should not ship"
+ readelf -d "${DESTDIR}/usr/sbin/smartctl" | grep -q 'libstdc++\.so\.6' \
+ || die "smartmontools: smartctl does not link libstdc++.so.6"
+}
diff --git a/packages/unosd/template b/packages/unosd/template
@@ -7,6 +7,7 @@ maintainer="finwo <finwo@pm.me>"
license="GPL-2.0-only"
homepage="https://unos.finwo.dev"
depends="busybox iproute2"
+hostmakedepends="dep"
do_build() {
# Single multicall binary (src/main.c dispatch + src/cli/*.c constructors)