SchOS/tools/posix/mkaroot.sh
Nexoniarz 4a7165c951 X gets a VT: keyboard, terminal switching, and a way out
Xorg uses VTs only when stat("/dev/tty0") reports Linux's tty major 4.
SchOS's terminals sit under major 1, so X ran on no VT at all: the console
kept drawing over it, Ctrl+Alt+F-keys did nothing, and nothing ended the
session. stat now reports terminals under Linux's numbers.

startx picks the VT only when uname says Linux, so root's xserverrc passes
the current one itself. An xorg.conf.d file names X's input devices, since
Alpine's Xorg looks them up through udev and SchOS has none yet.

Alt+F1..F6 and Ctrl+Alt+F1..F6 now switch terminals as on Linux, alongside
the settable chord. USB keyboards gain arrows, Home/End/PgUp/PgDn/Ins/Del
and the switch keys, which the PS/2 path always had.

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

247 lines
10 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
# The VT X runs on is the one startx was started from. startx works that out
# only when `uname -s` says Linux, and SchOS's says SchIFT.
vt=$(tty | sed -n 's|^/dev/tty\([0-9][0-9]*\)$|vt\1|p')
exec /usr/bin/X -nolisten tcp -dumbSched $vt "$@"
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
# X's input devices, named rather than found. Alpine's Xorg asks udev for
# them, SchOS has no udev, and X came up with no keyboard and no mouse: the
# pointer did not move, Ctrl+Alt+F-keys did nothing and nothing could end the
# session. SchOS has exactly two input nodes whatever the machine -- every
# keyboard on event0, every mouse on event1 (kernel/dev/input.c) -- so a
# fixed list is the whole answer. The file is SchOS's, in xorg.conf.d, which
# no Alpine package writes into.
mkdir -p "$P/etc/X11/xorg.conf.d"
cat > "$P/etc/X11/xorg.conf.d/10-schos.conf" <<'XEOF'
# SchOS has no udev, so X is told its input devices instead of finding them.
# event0 is every keyboard and event1 every mouse (kernel/dev/input.c).
Section "ServerFlags"
Option "AutoAddDevices" "false"
# Ctrl+Alt+Backspace ends X.
Option "DontZap" "false"
EndSection
Section "InputDevice"
Identifier "keyboard"
Driver "evdev"
Option "Device" "/dev/input/event0"
Option "XkbRules" "evdev"
Option "XkbOptions" "terminate:ctrl_alt_bksp"
EndSection
Section "InputDevice"
Identifier "mouse"
Driver "evdev"
Option "Device" "/dev/input/event1"
EndSection
Section "Device"
Identifier "fb"
Driver "fbdev"
EndSection
Section "Screen"
Identifier "screen"
Device "fb"
EndSection
Section "ServerLayout"
Identifier "schos"
Screen "screen"
InputDevice "keyboard" "CoreKeyboard"
InputDevice "mouse" "CorePointer"
EndSection
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"