From 406cff2ec095be0af36f2dc567c9ff9b78648d74 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sat, 24 Oct 2026 15:28:32 +0200 Subject: [PATCH] build: an apk-managed root, with SchOS as a package make aroot builds a SchFS root from Alpine's minirootfs, the packages ports/alpine/world lists, and SchOS itself as the signed apk package schos-base, in its own repository inside the root. apk add installs from Alpine; apk upgrade updates both. Booted: zsh is Alpine's, apk update sees 28669 packages, apk add htop works, and so does xorg-server. world.lock pins every package with its sha256; a build fetches only what dist/alpine/ lacks and never looks for newer versions. make alpine-lock is the one step that does. Alpine's zsh, busybox commands and hwdata replace SchOS's copies; SchOS keeps its own tools, toybox's other commands, and static mksh, which schinit's recovery shell now tries first. Also: the dependency-file include matched Alpine's usr/lib/sysctl.d, a directory, and stopped every make. See docs/plan/alpine.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4 --- Makefile | 51 ++++++++++- cmd/schinit/main.c | 5 ++ docs/plan/README.md | 1 + docs/plan/alpine.md | 73 ++++++++++++++++ ports/alpine/Makefile | 80 +++++++++++++++++ ports/alpine/world | 18 ++++ ports/alpine/world.lock | 12 +++ tools/posix/alpine-lock.sh | 62 +++++++++++++ tools/posix/mkaroot.sh | 172 +++++++++++++++++++++++++++++++++++++ 9 files changed, 473 insertions(+), 1 deletion(-) create mode 100644 docs/plan/alpine.md create mode 100644 ports/alpine/Makefile create mode 100644 ports/alpine/world create mode 100644 ports/alpine/world.lock create mode 100644 tools/posix/alpine-lock.sh create mode 100644 tools/posix/mkaroot.sh diff --git a/Makefile b/Makefile index 9a56822..c465fef 100644 --- a/Makefile +++ b/Makefile @@ -1555,6 +1555,52 @@ $(BUILD)/schfs.img: $(MKSCHFS) $(BUILD)/sysroot.stamp @rm -f $@ ./$(MKSCHFS) $@ --size $(SCHFS_BLOCKS) --label schos --sysroot $(SYSROOT) +# --- The apk-managed root ------------------------------------------------------ +# +# Alpine's minirootfs, the packages ports/alpine/world names, and SchOS as the +# apk package schos-base -- a root that `apk add` and `apk upgrade` manage. +# See tools/posix/mkaroot.sh for what goes where and why. +# +# make aroot build/aroot.img, a SchFS root volume of AROOT_BLOCKS +# make qemu-aroot boot it as the root disk (the disk-root kernel) +# make alpine-lock re-resolve ports/alpine/world against Alpine's mirror; +# the one step that looks for newer packages +# +# The build itself is offline once dist/ has the files world.lock names. +AROOT_BLOCKS ?= 262144 +AROOT_DIR = $(BUILD)/aroot +AROOT_KEYS = $(BUILD)/apk-keys +APK_STATIC = $(BUILD)/ports/alpine/apk.static +ALPINE_MINI = dist/alpine-minirootfs-3.24.3-x86_64.tar.gz +SCHOS_PKGVER ?= 0.1_git$(shell git log -1 --format=%cd --date=format:%Y%m%d%H%M 2>/dev/null || echo 0)-r0 + +alpine-port: + $(MAKE) -C ports/alpine install + +alpine-lock: + $(MAKE) -C ports/alpine lock + +$(AROOT_DIR)/stamp: $(BUILD)/sysroot.stamp ports/alpine/world.lock tools/posix/mkaroot.sh | alpine-port + sh tools/posix/mkaroot.sh $(SYSROOT) $(AROOT_DIR) $(APK_STATIC) $(ALPINE_MINI) \ + $(AROOT_KEYS) $(SCHOS_PKGVER) ports/alpine/world.lock dist/alpine + touch $@ + +$(BUILD)/aroot.img: $(MKSCHFS) $(AROOT_DIR)/stamp + @rm -f $@ + ./$(MKSCHFS) $@ --size $(AROOT_BLOCKS) --label schos --sysroot $(AROOT_DIR)/root + +aroot: $(BUILD)/aroot.img + +qemu-aroot: esp $(BUILD)/aroot.img $(OVMF_CODE) $(OVMF_VARS) + $(QEMU) -machine pc-i440fx-9.2 \ + -drive if=pflash,format=raw,unit=0,readonly=on,file=$(OVMF_CODE) \ + -drive if=pflash,format=raw,unit=1,file=$(OVMF_VARS) \ + -drive file=fat:rw:$(BUILD)/esp,index=0,media=disk,format=raw \ + -drive file=$(BUILD)/aroot.img,format=raw,index=1,media=disk \ + -m 2G -smp 2 $(QEMU_ACCEL) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) $(QEMU_NET) $(QEMU_AUDIO) + +.PHONY: alpine-port alpine-lock aroot qemu-aroot + # Boot with a SchFS volume attached as the second disk. # # Needs the ramdisk-root kernel ("make ramroot" first): with RAMROOT the root @@ -1673,4 +1719,7 @@ distclean: clean # Everything under arch/ and schfs/ therefore had NO header dependency # tracking at all -- editing include/fs/schfs.h or include/arch/mmu.h left stale # objects that linked without complaint. find actually recurses. --include $(shell find $(BUILD) -name '*.d' 2>/dev/null) +# Files only, and not under the trees that are root filesystems rather than +# build output: Alpine's usr/lib/sysctl.d is a directory, and including it +# stopped every make with "Is a directory". +-include $(shell find $(BUILD) \( -path $(BUILD)/aroot -o -path $(BUILD)/iso -o -path $(BUILD)/ports \) -prune -o -type f -name '*.d' -print 2>/dev/null) diff --git a/cmd/schinit/main.c b/cmd/schinit/main.c index 60a889b..1bb9250 100644 --- a/cmd/schinit/main.c +++ b/cmd/schinit/main.c @@ -623,7 +623,11 @@ main(int argc, char *argv[]) ioctl(0, TIOCSCTTY, 0); char *sh[] = { (char *)"-zsh", 0 }; execve((char *)"/bin/zsh", sh, 0); + // mksh first: on an apk-managed root /bin/sh is busybox, which + // needs musl's dynamic linker, and the recovery shell should need + // nothing. On the ramdisk root mksh is /bin/sh and this misses. char *fb[] = { (char *)"sh", 0 }; + execve((char *)"/bin/mksh", fb, 0); execve((char *)"/bin/sh", fb, 0); exit(); } @@ -643,6 +647,7 @@ main(int argc, char *argv[]) if (fork() == 0) { setsid(); char *argv[] = { (char *)"sh", 0 }; + execve((char *)"/bin/mksh", argv, 0); execve((char *)"/bin/sh", argv, 0); exit(); } diff --git a/docs/plan/README.md b/docs/plan/README.md index 8a4ecd0..6f631b1 100644 --- a/docs/plan/README.md +++ b/docs/plan/README.md @@ -23,6 +23,7 @@ Taken together, because they turn out to need the same foundation. | | | |---|---| +| **[alpine.md](alpine.md)** | **Packages.** The root is managed by Alpine's apk: Alpine's binaries run unmodified, and SchOS itself is the apk package schos-base. | | **[xorg.md](xorg.md)** | **The desktop.** Xorg on the framebuffer with the fbdev and evdev drivers; twm, then Window Maker. Replaces wayland.md for Gate 3. | | **[wayland.md](wayland.md)** | *Not being worked on; see xorg.md.* The GUI stack becomes Wayland. SchWM and SchDE are **deleted** — actually gone from the tree, not marked; Schcom survives and changes its wire format. | | ~~**hybrid.md**~~ | **Built.** All four mechanisms exist, the GPU driver runs outside the kernel, it survives being killed, and the display mode can be changed from a process. H4 (USB) was *withdrawn* rather than left open — USB is the bus peripherals arrive on, not a peripheral. | diff --git a/docs/plan/alpine.md b/docs/plan/alpine.md new file mode 100644 index 0000000..a63a801 --- /dev/null +++ b/docs/plan/alpine.md @@ -0,0 +1,73 @@ +# apk: Alpine's packages on SchOS + +Alpine is musl with a different init, and SchOS is musl with a different +kernel. Alpine's binaries run on SchOS unmodified: its dynamic linker, its +busybox, and apk itself, fetching over TLS through SchOS's network stack. +So instead of porting programs one at a time, SchOS can install them. + +## The model + +The root filesystem is an apk-managed root: + +| Part | Where it comes from | +|---|---| +| musl, busybox, apk-tools, alpine-keys, CA certificates | Alpine's minirootfs (`ports/alpine`) | +| what `ports/alpine/world` lists (zsh, terminfo, ...) | Alpine's packages, pinned in `world.lock` | +| schinit, its jobs, SchOS's tools, toybox, mksh, configuration | **schos-base**, an apk package the build makes from `sysroot/` | + +`apk add` installs anything from Alpine's main and community repositories. +`apk upgrade` updates Alpine's packages, and schos-base from the SchOS +repository at `/var/lib/schos/repo`. + +PID 1 is still schinit. Alpine's OpenRC is not installed, and packages that +ship OpenRC services install them where nothing reads them. + +## Who owns what + +Where Alpine and SchOS both have a program, Alpine's wins, and SchOS keeps +only what is its own (see `tools/posix/mkaroot.sh`): + +- `/bin/sh`, `cat`, `sed`, `find` and the rest are busybox. Alpine's package + scripts are written against busybox. +- zsh is Alpine's. SchOS keeps its `zshrc` and `zshenv` (`replaces=zsh`). +- `/etc/os-release` and `/etc/motd` are SchOS's (`replaces=`). +- `passwd`, `group` and `profile` are Alpine's. schos-base's post-install + sets root's shell to zsh, and SchOS's profile is `/etc/profile.d/schos.sh`. +- mksh is `/bin/mksh`. schinit's recovery shell looks for it first because it + is static: a broken musl cannot take recovery down with it. + +## Building + + make aroot build/aroot.img: the root, as a SchFS volume + make qemu-aroot boot it + make alpine-lock re-resolve world against Alpine (needs the network) + +Only `alpine-lock` asks Alpine what is current. A normal build installs +exactly what `world.lock` names: files in `dist/alpine/`, fetched once and +checked against the lock's sha256, and against Alpine's signatures by apk. + +schos-base and its repository index are signed with a key made on the first +build (`build/apk-keys/`, never committed). The root trusts it beside +Alpine's keys. + +## Kernel work this needed + +All of it found by running apk: + +- **TCP receive window:** it reopened before the reader drained, so lwIP + refused data and the sender waited on retransmission timers (5 KB/s). It + now reopens on read, with a 32-segment window. +- **mmap address reuse:** freed ranges were never reused, so musl's malloc + used up the 256 mapping records with 780 MB of RAM free. +- **unlinkat / renameat:** bare `-1`, read as EPERM, where apk expects ENOENT; + renameat could not replace its target, and wrote v1 dirents into SchFS. +- **`#!` scripts in execve**, and ENOEXEC: apk runs triggers directly. +- **chroot**, and mounting devtmpfs and proc more than once. + +## Next + +- The live ISO and USB stick are still the ramdisk root. They need the apk + root on the stick itself, or an installer that writes it to a disk. +- Xorg from apk (`xorg-server xf86-video-fbdev xf86-input-evdev`) instead of + the hand port in [xorg.md](xorg.md). The packages install; running them is + the next test. diff --git a/ports/alpine/Makefile b/ports/alpine/Makefile new file mode 100644 index 0000000..dcf8a5c --- /dev/null +++ b/ports/alpine/Makefile @@ -0,0 +1,80 @@ +# Alpine Linux -- the base the apk-managed root is built on: https://alpinelinux.org/ +# +# Alpine is musl with a different init, which is what SchOS is, and its +# binaries run here unmodified -- Alpine's own dynamic linker, busybox, and +# apk with TLS. So rather than port each program by hand, the root becomes an +# Alpine root that apk manages, with SchOS's own programs as an apk package of +# their own (schos-base, built by tools/posix/mkaroot.sh). +# +# Two downloads, both pinned: +# the minirootfs musl, busybox, apk-tools, alpine-keys, ca-certificates: +# the smallest root apk can work in +# apk.static apk itself, static, for the build host: it makes the +# schos-base package and its index, and installs it into +# the root, without anything from Alpine on the host +# +# Nothing goes into sysroot/: this is input to the root image, not part of the +# ramdisk-root system. + +include ../mk/port.mk + +ALPINE_BRANCH := 3.24 +ALPINE_VERSION := 3.24.3 +ALPINE_MIRROR := https://dl-cdn.alpinelinux.org/alpine/v$(ALPINE_BRANCH) + +MINI := alpine-minirootfs-$(ALPINE_VERSION)-x86_64.tar.gz +MINI_URL := $(ALPINE_MIRROR)/releases/x86_64/$(MINI) +MINI_SHA256 := f8e1cafba9b1b283b5d30d06f9885c8d2bc161ffe9083ade8e2e157813a53407 + +APKS := apk-tools-static-3.0.8-r0.apk +APKS_URL := $(ALPINE_MIRROR)/main/x86_64/$(APKS) +# Recorded on first download. Alpine signs the package itself, but nothing on +# the build host could check that signature before apk.static exists. +APKS_SHA256 := c8e2c88c13ba12a12269b79a3543e1190ff8c0ab0beb32b58cadfd5881c619e3 + +APK_STATIC := $(PORTDIR)/apk.static + +$(DISTDIR)/$(MINI): | $(DISTDIR) + curl -fsSL -o $@ $(MINI_URL) + sh $(SHA256CHECK) "$(MINI_SHA256)" "$@" + +$(DISTDIR)/$(APKS): | $(DISTDIR) + curl -fsSL -o $@ $(APKS_URL) + sh $(SHA256CHECK) "$(APKS_SHA256)" "$@" + +download: $(DISTDIR)/$(MINI) $(DISTDIR)/$(APKS) + +# An .apk is gzipped tar streams back to back; tar reads through them and +# warns about the signature's odd headers, which is harmless. +$(APK_STATIC): $(DISTDIR)/$(APKS) + mkdir -p $(PORTDIR)/x + tar -xzf $< -C $(PORTDIR)/x 2>/dev/null || test -f $(PORTDIR)/x/sbin/apk.static + cp $(PORTDIR)/x/sbin/apk.static $@ + rm -rf $(PORTDIR)/x + +build install: $(APK_STATIC) $(DISTDIR)/$(MINI) world + +clean: + rm -rf $(PORTDIR) + +# world.lock's packages, into dist/alpine/. Only what is missing is fetched, +# and each file is checked against the lock: a build never asks Alpine what +# is newer. `make alpine-lock` at the top level is what does that. +WORLD_DIST := $(DISTDIR)/alpine + +world: | $(DISTDIR) + mkdir -p $(WORLD_DIST) + grep -v '^#' world.lock | while read -r f sum repo; do \ + if [ ! -f $(WORLD_DIST)/$$f ]; then \ + echo "alpine: fetching $$f"; \ + curl -fsSL -o $(WORLD_DIST)/$$f.part $(ALPINE_MIRROR)/$$repo/x86_64/$$f && \ + mv $(WORLD_DIST)/$$f.part $(WORLD_DIST)/$$f || exit 1; \ + fi; \ + sh $(SHA256CHECK) "$$sum" $(WORLD_DIST)/$$f >/dev/null || exit 1; \ + done + +lock: $(APK_STATIC) $(DISTDIR)/$(MINI) + sh $(TOPDIR)/tools/posix/alpine-lock.sh $(APK_STATIC) $(DISTDIR)/$(MINI) \ + world world.lock $(WORLD_DIST) + +.PHONY: world lock diff --git a/ports/alpine/world b/ports/alpine/world new file mode 100644 index 0000000..fab5aa1 --- /dev/null +++ b/ports/alpine/world @@ -0,0 +1,18 @@ +# Alpine packages built into the apk-managed root, one per line. +# +# Edit this, then run `make alpine-lock` to resolve it, with every dependency, +# into world.lock -- exact versions and checksums. A normal build installs +# exactly what world.lock says from dist/alpine/, downloading only files it +# does not have yet, and never asks Alpine what is newer. That happens only +# when you run `make alpine-lock` again. +# +# Anything else is one `apk add` away on the running system; this list is +# what a fresh image starts with. + +# The login shell. Replaces SchOS's own zsh port. +zsh +# Terminal descriptions for zsh, htop, and everything else that draws. +ncurses-terminfo-base +# The PCI and USB ID databases: lspci, fastfetch and Xorg read them. +hwdata-pci +hwdata-usb diff --git a/ports/alpine/world.lock b/ports/alpine/world.lock new file mode 100644 index 0000000..90005b1 --- /dev/null +++ b/ports/alpine/world.lock @@ -0,0 +1,12 @@ +# Generated by tools/posix/alpine-lock.sh from world. Do not edit; +# change world and run `make alpine-lock`. +# file sha256 repository +busybox-1.37.0-r31.apk 0e626fa97cf937fda7bba4ed4dde8e2f2dbb5083ac34e02180cec0c4eba9351b main +busybox-binsh-1.37.0-r31.apk 93f803ae5db6b729c0f66f4afba6e907ccc17d7d5f8b8c3d25bdb4636de2c48a main +hwdata-pci-0.408-r0.apk fe61404e6d2bd1e04c021d53641c0ebce97911084d77994e43a288a1278686c1 main +hwdata-usb-0.408-r0.apk 272d365cb05dd085dd8022154cf71e3f3b0aaddd41c1269eeaf939c9d38675c8 main +libcap2-2.78-r0.apk 42886a7b4bcde26347c712925c8dc7db202b6029ef9b8c7ee81577ab7bdd8fad main +libncursesw-6.6_p20260516-r0.apk cf8caa8a88bc4ce9d9e395567fefaf9ef7fbd55c50ae6155b35c1b58f3755023 main +musl-1.2.6-r3.apk e142255d0fa397f14318996bc10378093dcc069dc3dd1b6818c8f28a789c21da main +ncurses-terminfo-base-6.6_p20260516-r0.apk ce83be6d0bd10584a53c3a5868cc1595ce5f0f8e0b621f8f7f70b6144e521fc9 main +zsh-5.9-r7.apk 22ec3c4de69e38437e351ba5a881f5d88e9b518a248c8aa06d36b148cf16ce32 main diff --git a/tools/posix/alpine-lock.sh b/tools/posix/alpine-lock.sh new file mode 100644 index 0000000..ec65b11 --- /dev/null +++ b/tools/posix/alpine-lock.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# Resolve ports/alpine/world into world.lock. +# +# tools/posix/alpine-lock.sh APK_STATIC MINIROOTFS WORLD LOCK DISTDIR +# +# Asks Alpine's mirror, so it needs the network; nothing else in the build +# does. Every package in the dependency closure is fetched into DISTDIR and +# recorded as +# +# +# +# which is all a build needs to download it again and know it got the same +# bytes. apk checks Alpine's signature on top of that when it installs. + +set -eu +[ $# -eq 5 ] || { echo "usage: $0 APK_STATIC MINIROOTFS WORLD LOCK DISTDIR" >&2; exit 2; } +abs() { case "$1" in /*) echo "$1" ;; *) echo "$(pwd)/$1" ;; esac; } +APK=$(abs "$1"); MINI=$(abs "$2"); WORLD=$(abs "$3"); LOCK=$(abs "$4"); DIST=$(abs "$5") + +T=$(mktemp -d) +trap 'rm -rf "$T"' EXIT +mkdir -p "$T/root" "$DIST" +tar -xzf "$MINI" -C "$T/root" +# apk.static looks for CA certificates on the host, where a NixOS or a macOS +# keeps them somewhere else; Alpine's own bundle is right here. +export SSL_CERT_FILE="$T/root/etc/ssl/certs/ca-certificates.crt" + +PKGS=$(sed 's/#.*//' "$WORLD" | tr -s ' \t' '\n' | grep . || true) +[ -n "$PKGS" ] || { echo "alpine-lock: $WORLD lists nothing" >&2; exit 1; } + +"$APK" --root "$T/root" --no-cache update -q +# shellcheck disable=SC2086 +"$APK" --root "$T/root" --no-cache fetch --recursive --output "$DIST" $PKGS + +# Which repository each one came from, read from the indexes apk just used. +MIRROR=$(grep -m1 '^http' "$T/root/etc/apk/repositories" | sed 's|/[^/]*$||') +for r in main community; do + curl -fsSL "$MIRROR/$r/x86_64/APKINDEX.tar.gz" | tar -xzO APKINDEX | + awk -v r="$r" '/^P:/{p=substr($0,3)} /^V:/{print p "-" substr($0,3) ".apk", r}' +done > "$T/where" + +# The closure, as apk resolved it: install it into the scratch root and read +# back what is there. +# shellcheck disable=SC2086 +"$APK" --root "$T/root" --no-cache --no-scripts --no-chown add -q $PKGS +"$APK" --root "$T/root" info -v 2>/dev/null | sort > "$T/closure" + +{ + echo "# Generated by tools/posix/alpine-lock.sh from world. Do not edit;" + echo "# change world and run \`make alpine-lock\`." + echo "# file sha256 repository" + while read -r nv; do + f=$nv.apk + [ -f "$DIST/$f" ] || continue # in the minirootfs, not needed by world + repo=$(awk -v f="$f" '$1 == f { print $2; exit }' "$T/where") + [ -n "$repo" ] || { echo "alpine-lock: $f is in no repository" >&2; exit 1; } + sum=$(sha256sum "$DIST/$f" | cut -d' ' -f1) + echo "$f $sum $repo" + done < "$T/closure" +} > "$LOCK.new" +mv "$LOCK.new" "$LOCK" +echo "alpine-lock: $(grep -vc '^#' "$LOCK") packages in $LOCK" diff --git a/tools/posix/mkaroot.sh b/tools/posix/mkaroot.sh new file mode 100644 index 0000000..d3499cc --- /dev/null +++ b/tools/posix/mkaroot.sh @@ -0,0 +1,172 @@ +#!/bin/sh +# Build the apk-managed root: Alpine's minirootfs, with SchOS installed into +# it as an apk package. +# +# tools/posix/mkaroot.sh SYSROOT OUTDIR APK_STATIC MINIROOTFS KEYDIR VERSION \ +# LOCK ALPINEDIR +# +# SYSROOT the staged SchOS userland (sysroot/), what the ramdisk root is +# OUTDIR gets root/ (the tree for mkschfs) and repo/ (the SchOS repository) +# APK_STATIC Alpine's static apk, from ports/alpine +# MINIROOTFS Alpine's minirootfs tarball, from ports/alpine +# KEYDIR where the repository signing key lives; made on first use +# VERSION schos-base's apk version, e.g. 0.1_git20261024-r0 +# LOCK ports/alpine/world.lock: the Alpine packages to install +# ALPINEDIR where those are, already fetched and checked (dist/alpine) +# +# What comes out is a root apk can manage: `apk add` installs anything from +# Alpine, and `apk upgrade` updates SchOS from its own repository, which is +# copied into the root at /var/lib/schos/repo and listed after Alpine's. +# +# schos-base is the sysroot less what Alpine's own packages own. Those are: +# +# /bin/sh busybox-binsh. Alpine's package scripts are written for +# ash; mksh stays, as /bin/mksh, and schinit's recovery +# shell looks for it first because it is static. +# /lib/ld-musl-* musl. Every SchOS program is static and does not use it. +# /etc/passwd, /etc/group +# alpine-baselayout-data. Root's shell becomes zsh below, +# the one edit SchOS makes to them. +# /etc/profile alpine-baselayout-data. SchOS's goes to profile.d. +# +# /etc/os-release and /etc/motd are SchOS's and taken over with `replaces`. +# Alpine's programs replace SchOS's where both have one: +# +# zsh Alpine's, from world. SchOS keeps its zshrc and zshenv, +# taken over with `replaces`. +# pci.ids, usb.ids Alpine's hwdata-pci and hwdata-usb, from world; Xorg +# depends on hwdata-pci and two owners of one file is an +# error apk will not install past. +# busybox's commands cat, sed, find and the rest: Alpine's package scripts +# are written against busybox's, and toybox's same-named +# links are left out. toybox keeps what busybox lacks. +# +# Runs as an ordinary user. apk's scripts need a chroot, so the install runs +# with --no-scripts and this script makes the one change schos-base's +# post-install would; on a running SchOS the script itself runs. + +set -eu + +[ $# -eq 8 ] || { echo "usage: $0 SYSROOT OUTDIR APK_STATIC MINIROOTFS KEYDIR VERSION LOCK ALPINEDIR" >&2; exit 2; } +abs() { case "$1" in /*) echo "$1" ;; *) echo "$(pwd)/$1" ;; esac; } +SYSROOT=$(abs "$1"); OUT=$(abs "$2"); APK=$(abs "$3") +MINI=$(abs "$4"); KEYDIR=$(abs "$5"); VERSION=$6 +LOCK=$(abs "$7"); ADIR=$(abs "$8") + +KEY=$KEYDIR/schos-local.rsa +ROOT=$OUT/root +REPO=$OUT/repo/x86_64 +PKG=$OUT/pkg + +# --- the signing key ---------------------------------------------------------- +# +# One per build tree, never committed: whoever builds SchOS signs their own +# packages, and the root they build trusts exactly that key besides Alpine's. +if [ ! -f "$KEY" ]; then + mkdir -p "$KEYDIR" + openssl genrsa -out "$KEY" 2048 2>/dev/null + openssl rsa -in "$KEY" -pubout -out "$KEY.pub" 2>/dev/null + echo "mkaroot: made repository key $KEY" +fi + +rm -rf "$OUT" +mkdir -p "$ROOT" "$REPO" "$PKG/root" +# First, because what busybox provides is read from it. +tar -xzf "$MINI" -C "$ROOT" + +# --- schos-base --------------------------------------------------------------- + +cp -a "$SYSROOT/." "$PKG/root/" +P=$PKG/root +mv "$P/bin/sh" "$P/bin/mksh" +rm -f "$P"/lib/ld-musl-*.so.1 "$P/etc/passwd" "$P/etc/group" +rm -rf "$P/bin/zsh" "$P/bin/zsh-5.9" "$P/bin/zsh.old" "$P/usr/share/zsh" +# pci.ids and usb.ids are Alpine's hwdata-pci and hwdata-usb, in world. +rm -rf "$P/usr/share/hwdata" + +# Every busybox link in the minirootfs, out of the package. +(cd "$ROOT" && find bin sbin usr/bin usr/sbin -type l) | while read -r l; do + case "$(readlink "$ROOT/$l")" in + */busybox) rm -f "$P/$l" ;; + esac +done + +# SchOS's profile without the three lines Alpine's already sets, and sets +# better: PATH with sbin, HOME from passwd, TERM from the console job. +mkdir -p "$P/etc/profile.d" +grep -v '^export \(PATH\|HOME\|TERM\)=' "$P/etc/profile" > "$P/etc/profile.d/schos.sh" +rm -f "$P/etc/profile" + +# Mount points and runtime directories are alpine-baselayout's; an empty +# directory here only duplicates it. +for d in dev proc sys run tmp srv; do + rmdir "$P/$d" 2>/dev/null || true +done + +cat > "$PKG/post-install" <<'EOF' +#!/bin/sh +# Root logs in to zsh, as on every SchOS; Alpine's passwd says /bin/sh. +sed -i 's|^\(root:[^:]*:0:0:[^:]*:[^:]*:\)/bin/sh$|\1/bin/zsh|' /etc/passwd +for s in /bin/zsh /bin/mksh; do + grep -qx "$s" /etc/shells 2>/dev/null || echo "$s" >> /etc/shells +done +exit 0 +EOF +cp "$PKG/post-install" "$PKG/post-upgrade" + +"$APK" mkpkg \ + --info name:schos-base \ + --info "version:$VERSION" \ + --info arch:x86_64 \ + --info "description:SchOS: init, console, tools and configuration" \ + --info license:GPL-2.0-or-later \ + --info url:https://github.com/Nexoniarz/SchOS \ + --info origin:schos-base \ + --info "depends:alpine-baselayout busybox musl apk-tools zsh" \ + --info "replaces:alpine-release alpine-baselayout zsh" \ + --files "$P" \ + --script "post-install:$PKG/post-install" \ + --script "post-upgrade:$PKG/post-upgrade" \ + --sign-key "$KEY" \ + --output "$REPO/schos-base-$VERSION.apk" + +"$APK" --keys-dir "$KEYDIR" mkndx --sign-key "$KEY" \ + --output "$REPO/APKINDEX.tar.gz" "$REPO"/*.apk + +# --- the root ----------------------------------------------------------------- + +cp "$KEY.pub" "$ROOT/etc/apk/keys/" + +# world.lock's packages, by file: no index and no network. apk still checks +# each one's signature against Alpine's keys in the root. +FILES=$(grep -v '^#' "$LOCK" | while read -r f sum repo; do echo "$ADIR/$f"; done) +# shellcheck disable=SC2086 +"$APK" --root "$ROOT" --repositories-file /dev/null --repository "$OUT/repo" \ + --no-network --no-scripts --no-chown --no-cache add $FILES schos-base + +# What world asked for, by name rather than by file, so a later `apk upgrade` +# on the running system follows Alpine rather than these exact versions. +sed 's/#.*//' "$(dirname "$LOCK")/world" | tr -s ' \t' '\n' | grep . | + while read -r w; do grep -qx "$w" "$ROOT/etc/apk/world" || echo "$w" >> "$ROOT/etc/apk/world"; done +# Installing by file pins each one to its exact checksum ("zsh>/dev/null || echo "$s" >> "$ROOT_PI/etc/shells" + done' + +# The repository goes in too, so the installed system can reinstall and +# upgrade schos-base, and Alpine's are listed for everything else. +mkdir -p "$ROOT/var/lib/schos" +cp -a "$OUT/repo" "$ROOT/var/lib/schos/repo" +{ + cat "$ROOT/etc/apk/repositories" + echo "/var/lib/schos/repo" +} > "$ROOT/etc/apk/repositories.new" +mv "$ROOT/etc/apk/repositories.new" "$ROOT/etc/apk/repositories" + +echo "mkaroot: $ROOT ($(du -sh "$ROOT" | cut -f1)), schos-base $VERSION"