udevd and a one-shot trigger run from schinit, 10-schos.conf no longer lists event0/event1, and the docs say what udev covers and what it does not. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
346 lines
14 KiB
Shell
346 lines
14 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. (It used to add
|
|
# -dumbSched, Xorg's switch for reading input on the main loop, because when SchOS
|
|
# had no threads the server crashed on its way out the first time a client
|
|
# disconnected, in pthread_join on a thread that was never made. SchOS has threads
|
|
# now; X was run without the switch under QEMU -- clients connecting and
|
|
# disconnecting, an xterm starting and exiting -- and it stays up.) 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 $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 are found, not named: udevd (etc/schinit/udevd.job) turns
|
|
# the kernel's uevents into /run/udev data, and Xorg asks libudev for every
|
|
# device with ID_INPUT set. That was a hand-written list of event0 and event1
|
|
# while SchOS had no udev. The file is SchOS's, in xorg.conf.d, which no Alpine
|
|
# package writes into; what is left here is the options the defaults do not
|
|
# give, and the framebuffer driver.
|
|
mkdir -p "$P/etc/X11/xorg.conf.d"
|
|
cat > "$P/etc/X11/xorg.conf.d/10-schos.conf" <<'XEOF'
|
|
Section "ServerFlags"
|
|
# Ctrl+Alt+Backspace ends X.
|
|
Option "DontZap" "false"
|
|
EndSection
|
|
|
|
Section "InputClass"
|
|
Identifier "SchOS keyboards"
|
|
MatchIsKeyboard "on"
|
|
Option "XkbRules" "evdev"
|
|
Option "XkbOptions" "terminate:ctrl_alt_bksp"
|
|
EndSection
|
|
|
|
Section "Device"
|
|
Identifier "fb"
|
|
Driver "fbdev"
|
|
EndSection
|
|
|
|
Section "Screen"
|
|
Identifier "screen"
|
|
Device "fb"
|
|
EndSection
|
|
|
|
Section "ServerLayout"
|
|
Identifier "schos"
|
|
Screen "screen"
|
|
EndSection
|
|
XEOF
|
|
|
|
# bwrap, without the sandbox. glycin -- which loads every image gdk-pixbuf is
|
|
# asked for, so every GTK icon -- runs its loaders under bubblewrap and has no
|
|
# other way to start them. Real bwrap needs user and mount namespaces, which
|
|
# SchOS does not have. This takes the same arguments, keeps what they say
|
|
# about the environment and the directory, and runs the program as it is:
|
|
# no isolation at all, and it says so in its name. It replaces bubblewrap's own
|
|
# /usr/bin/bwrap, which glycin depends on and which cannot run here.
|
|
mkdir -p "$P/usr/bin"
|
|
cat > "$P/usr/bin/bwrap" <<'BEOF'
|
|
#!/bin/sh
|
|
# SchOS: bubblewrap's command line, with no sandbox behind it. SchOS has no
|
|
# namespaces; what the options say about the environment and the working
|
|
# directory is honoured, and everything about isolation is ignored.
|
|
clear= envs=
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--clearenv) clear=1; shift ;;
|
|
--setenv) envs="$envs $2=$3"; shift 3 ;;
|
|
--unsetenv) unset "$2"; shift 2 ;;
|
|
--chdir) cd "$2" || exit 1; shift 2 ;;
|
|
--bind|--bind-try|--ro-bind|--ro-bind-try|--dev-bind|--dev-bind-try|\
|
|
--symlink|--bind-data|--ro-bind-data|--file|--chmod) shift 3 ;;
|
|
--dev|--proc|--tmpfs|--dir|--mqueue|--remount-ro|--seccomp|--add-seccomp-fd|\
|
|
--uid|--gid|--hostname|--cap-add|--cap-drop|--lock-file|--sync-fd|\
|
|
--info-fd|--json-status-fd|--block-fd|--userns-block-fd|--perms|--size|\
|
|
--argv0|--exec-label|--file-label|--userns|--userns2|--pidns) shift 2 ;;
|
|
--unshare-*|--share-net|--die-with-parent|--new-session|--as-pid-1|\
|
|
--disable-userns|--assert-userns-disabled) shift ;;
|
|
--) shift; break ;;
|
|
-*) echo "bwrap: $1: not understood (SchOS runs bwrap without a sandbox)" >&2
|
|
exit 1 ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
[ $# -gt 0 ] || { echo "bwrap: no command" >&2; exit 1; }
|
|
# shellcheck disable=SC2086
|
|
if [ -n "$clear" ]; then exec env -i PATH="$PATH" $envs "$@"; fi
|
|
# shellcheck disable=SC2086
|
|
[ -n "$envs" ] && exec env $envs "$@"
|
|
exec "$@"
|
|
BEOF
|
|
chmod 755 "$P/usr/bin/bwrap"
|
|
|
|
# ALSA's default device, through the OSS plugin to /dev/dsp. The plug in front
|
|
# converts whatever a program plays to what every SchOS audio driver accepts:
|
|
# 16-bit stereo at 48 kHz (AC'97 without variable rate plays nothing else, and
|
|
# the plugin refuses a rate the device rounds by more than 1%).
|
|
mkdir -p "$P/etc"
|
|
cat > "$P/etc/asound.conf" <<'AEOF'
|
|
# SchOS: ALSA plays through OSS's /dev/dsp (kernel/dev/audio.c).
|
|
pcm.!default {
|
|
type plug
|
|
slave {
|
|
pcm "dsp"
|
|
format S16_LE
|
|
channels 2
|
|
rate 48000
|
|
}
|
|
}
|
|
pcm.dsp {
|
|
type oss
|
|
device /dev/dsp
|
|
}
|
|
ctl.!default {
|
|
type hw
|
|
card 0
|
|
}
|
|
AEOF
|
|
|
|
# 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 bubblewrap" \
|
|
--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 triggers --no-scripts skipped, and they are not optional: they build the
|
|
# caches half the desktop reads. Without shared-mime-info's, GLib cannot tell a
|
|
# PNG from anything else, so gdk-pixbuf -- which hands images to glycin by MIME
|
|
# type -- refused every icon and NetSurf died on its first toolbar button.
|
|
# Likewise gdk-pixbuf's loader list, GLib's compiled schemas and GIO modules,
|
|
# GTK's input methods, the font caches and busybox's applet links.
|
|
#
|
|
# They run in the root as /, which is what bubblewrap is for: no chroot, so no
|
|
# root needed. apk names each script by the hex form of the checksum the
|
|
# triggers database keys in base64, and passes the paths it watches as
|
|
# arguments, globs expanded.
|
|
if command -v bwrap >/dev/null 2>&1; then
|
|
TRIG=$(mktemp -d)
|
|
tar -xzf "$ROOT/lib/apk/db/scripts.tar.gz" -C "$TRIG" --wildcards '*.trigger' 2>/dev/null || true
|
|
mkdir -p "$ROOT/tmp"
|
|
for t in "$TRIG"/*.trigger; do
|
|
[ -e "$t" ] || continue
|
|
hex=${t##*.X1}; hex=${hex%.trigger}
|
|
q=Q1$(python3 -c 'import base64,sys; print(base64.b64encode(bytes.fromhex(sys.argv[1])).decode())' "$hex")
|
|
paths=$(grep "^$q " "$ROOT/lib/apk/db/triggers" | cut -d' ' -f2-)
|
|
cp "$t" "$ROOT/tmp/apk-trigger"
|
|
# --clearenv: the host's environment is not the root's. A Nix shell
|
|
# exports GDK_PIXBUF_MODULE_FILE, and the loader cache went there.
|
|
bwrap --bind "$ROOT" / --dev /dev --proc /proc --clearenv \
|
|
--setenv PATH /usr/sbin:/usr/bin:/sbin:/bin --setenv HOME /root \
|
|
/bin/sh -c 'a=; for g in '"$paths"'; do for p in $g; do [ -e "$p" ] && a="$a $p"; done; done
|
|
exec /bin/sh /tmp/apk-trigger $a' \
|
|
|| echo "mkaroot: trigger $(basename "$t") failed" >&2
|
|
rm -f "$ROOT/tmp/apk-trigger"
|
|
done
|
|
rm -rf "$TRIG"
|
|
else
|
|
echo "mkaroot: no bwrap -- triggers not run; image and MIME caches are missing" >&2
|
|
fi
|
|
|
|
# The shared scratch directories, world-writable and sticky as baselayout
|
|
# means them. apk running as an ordinary user on the build host lands them as
|
|
# 0755, and with the kernel reporting real modes that is what tmux and every
|
|
# other program checking /tmp would see.
|
|
chmod 1777 "$ROOT/tmp" "$ROOT/var/tmp"
|
|
|
|
# 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"
|