SchOS/docs/architecture/packages.md
Nexoniarz 42008c26c0 docs: packages.md counts what world.lock actually holds
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 08:08:25 +01:00

5.6 KiB

Packages: an apk root

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 installs 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, Xorg, Window Maker, htop, NetSurf …) 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 (tools/posix/mkaroot.sh says so in its header):

  • /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.
  • pci.ids and usb.ids are Alpine's (hwdata-pci, hwdata-usb): Xorg depends on hwdata-pci, and two owners of one file is an error apk will not install past.
  • 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 qemu           boots build/schfs.img, the root as a 1 GB SchFS disk
make iso            the same root as the live system's compressed ramdisk
make alpine-lock    re-resolve world against Alpine (needs the network)

It is the only root; sysroot/ is what schos-base is packaged from. 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 — and never asks what is newer. world.lock resolves the thirty-odd names in world into about 180 packages, every dependency included, so every image after the first is reproducible.

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. The install runs as an ordinary user with --no-scripts, because apk's scripts want a chroot; mkaroot.sh does by hand the one change schos-base's post-install would have made.

What it took from the kernel

All of it found by running apk, and none of it specific to 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 reopens on read now, with a 32-segment window; downloads run at tens of megabits.
  • mmap address reuse: freed ranges were never reused, so musl's allocator used up the 256 mapping records with 780 MB of RAM free.
  • unlinkat / renameat returned a bare -1, read as EPERM, where apk expects ENOENT; renameat could not replace its target.
  • #! scripts in execve, and ENOEXEC: apk runs triggers directly.
  • chroot, and mounting devtmpfs and proc more than once.
  • Later, as bigger programs arrived: dynamic linking, threads and futex, demand paging, pseudo-terminals, inotify, flock, Unix-socket credentials. Each is in kernel.md or syscalls.md because a package asked for it.

The live system

The live ISO runs the apk root from RAM. The ramdisk is the compressed image (scff.md); at boot the volume grows to half of the free memory (4 GB of root on an 8 GB machine), backed by pages as they are written. Xorg and its dependencies are in the image already; anything else installs into that space in about a minute, over the network, and is gone at reboot.

What it does not do

  • No installer. Nothing is written to the machine's disks, so packages do not survive a reboot. Keeping them needs an installer that writes the root to a SATA or NVMe disk, and a kernel that finds its root there by the SchFS GPT type rather than as IDE disk 1.
  • Not every Alpine package runs. The ones in world were chosen because they run: terminal programs that draw and poll (htop, btop, tmux, mc), X clients, SDL 2 games, NetSurf on GTK 3. Others hit whatever the kernel does not do yet; Epiphany (WebKit) is one — its web-process child dies at start-up, and it has not been run to ground. A package is one apk add away, and finding what it needs is how most of the kernel's Linux surface got written.
  • Fortified libc is strict, and Alpine's packages are not always correct. lbreakout2 hands vsnprintf a size larger than its buffer and traps on Alpine too; world carries lbreakouthd instead.
  • apk's scripts and triggers do not run at build time (--no-scripts), so a package whose script matters has to have it done by hand in mkaroot.sh.
  • OpenRC services do nothing. Anything that expects an init script to start it has to be given a schinit job (init.md).