SchOS/tools/posix/mkaroot.sh
Nexoniarz df1394eaa3 SCFF: the live root ships compressed, with Xorg and Window Maker
SCFF, the SchOS Compressed File Format (include/fs/scff.h), is a block
image cut into 128 KB chunks, each its own zstd frame, behind an index.
tools/mkscff writes it from fs-ram.img on every core (-19, 11 s) and
checks each chunk decompresses to itself. The kernel carries the .scff
as its ramdisk and unpacks a chunk the first time a block in it is
read; zero blocks share one page. The kernel and the ISO are the
compressed size, and the root costs memory only for what is touched.
See docs/architecture/scff.md.

zstd 1.5.7 comes from ports/zstd like uACPI: the decoder is compiled
into the kernel at -O2, with a static context and a 64 KB stack of its
own, and mkscff builds the whole library from the same release.

world now has xorg-server, the fbdev and evdev drivers, xinit, Window
Maker, xterm and DejaVu, and /root/.xinitrc runs wmaker. That root is
358 MB, 102 MB as SCFF.

The ISO no longer carries the root a third time as an ISO 9660 tree,
which nothing read. schos.iso: 1389 MB -> 318 MB. El Torito keeps its
own copy of the EFI image, the layout real machines have booted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 21:17:31 +02:00

195 lines
8.4 KiB
Shell

#!/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"
# How startx starts the X server for root: Alpine's xserverrc, plus
# -dumbSched. Xorg reads input on a thread of its own, SchOS has no threads
# yet, and the server crashed on its way out the first time a client
# disconnected, in pthread_join on a thread that was never made. -dumbSched is
# Xorg's own switch for input on the main loop. The file is root's own rather
# than /etc/X11/xinit/xserverrc, which belongs to the xinit package.
mkdir -p "$P/root"
cat > "$P/root/.xserverrc" <<'XEOF'
#!/bin/sh
exec /usr/bin/X -nolisten tcp -dumbSched "$@"
XEOF
# What startx runs inside it: Window Maker, which world installs. Without this
# xinit falls back to twm and an xterm, neither of which is in the image.
cat > "$P/root/.xinitrc" <<'XEOF'
#!/bin/sh
exec wmaker
XEOF
# 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
# The machine's own name, which Alpine's hosts leaves out. Without it xauth
# cannot resolve "schos:0", and startx says "bad display name" four times.
grep -qw schos /etc/hosts || printf '127.0.1.1\tschos\n' >> /etc/hosts
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><Q1..."); none
# of them should stay pinned, and the dependencies among them need no entry.
sed -i '/></d' "$ROOT/etc/apk/world"
# The post-install above, done by hand because --no-scripts skipped it.
ROOT_PI=$ROOT sh -c '
sed -i "s|^\(root:[^:]*:0:0:[^:]*:[^:]*:\)/bin/sh$|\1/bin/zsh|" "$ROOT_PI/etc/passwd"
for s in /bin/zsh /bin/mksh; do
grep -qx "$s" "$ROOT_PI/etc/shells" 2>/dev/null || echo "$s" >> "$ROOT_PI/etc/shells"
done
grep -qw schos "$ROOT_PI/etc/hosts" || printf "127.0.1.1\tschos\n" >> "$ROOT_PI/etc/hosts"'
# 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"