SchOS/ports/toybox/Makefile
Nexoniarz b08432b2c3 userland: 268 applets, an editor, names that resolve, and a web server
Four complaints, and only one of them was the thing it looked like.

**"toybox is too small."** Half true, and the fix cost no licensing at all:
236 of toybox's applets are what defconfig builds, and the rest live in
toys/pending/ -- awk, bc, diff, more, expr, tr, hexdump, stty among them. Same
0BSD tarball, switched off by a config default. 26 of them are on now, 236 ->
268. Not everything: strace wants ptrace, syslogd wants BSD's facilitynames[],
git wants OpenSSL, and those three break the link.

The toybox build also works again on this machine. Its scripts start
#!/bin/bash, which does not exist on NixOS, so the binary in the tree was one
nobody could reproduce; the shebangs are rewritten to /usr/bin/env bash on the
way in. Pending applets are enabled by editing .config directly, because
`make oldconfig` is the obvious way and undoes it -- every pending symbol is
`default n`.

**BusyBox was asked for and is not here.** It is GPL-2.0-only, and `make
ramroot` links the root image *into the kernel binary*, so it would put GPLv2
inside an Apache-2.0 executable -- which is the exact case ports/Makefile's
NOTPORTS filter and docs/licensing.md §4 exist for. nano is the same answer one
version later. Both recorded in licensing.md with what was done instead, since
"no" without an alternative is how a rule gets quietly broken later.

**An editor that is not vi.** kilo, BSD-2-Clause, one 1300-line file, no
dependencies beyond termios and TIOCGWINSZ, both of which zsh already needed.
Installed as /bin/edit as well as /bin/kilo, because somebody with a file to
fix will type `edit`.

**"wget doesn't work."** Not a userland problem, and BusyBox's wget would have
failed identically: nothing on this system could turn a name into an address,
so every program taking a hostname failed in its own way and each looked like
a separate bug. There is still no DNS client in SchOS and there does not need
to be -- musl has a complete resolver and it works over ordinary UDP sockets.
The one missing fact was which nameserver, which the kernel has had all along
from DHCP and never published. /proc/resolv.conf publishes it in resolv.conf's
exact syntax; cmd/resolvconf decides whether it becomes /etc/resolv.conf, and
will not overwrite a hand-written one without -f.

  # wget -O - http://example.com/ | head -1
  <!doctype html><html lang="en"><head><title>Example Domain</title>

**Something to host with.** cmd/httpd: GET and HEAD, static files, index.html
for a directory and a listing otherwise, a child per connection. No CGI, no
keep-alive, no TLS, no config file. The path check is the only
security-relevant code in it and it refuses rather than normalises -- no
component may be `..`, and percent-decoding happens first, because %2e%2e is
`..` and a check before decoding does not see it. Both forms answer 403.

And the disk is 100 MB, from 32. It is sparse, so the image is 21 MB on the
host. RAMDISK_BLOCKS went 6000 -> 8000 with it: the ramdisk was at 86% and the
next thing added would have broken `make ramroot`, which nobody would have
noticed until they used it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-14 19:24:55 +02:00

141 lines
6.4 KiB
Makefile

# SPDX-License-Identifier: Apache-2.0
# toybox — 0BSD-licensed: https://landley.net/toybox/
# Provides SchOS's core utilities (ls, cat, cp, mv, rm, mkdir, ...) as a
# single multicall binary, installed under sysroot/bin/ once per applet name.
#
# mkfs (tools/mkfs.c) doesn't understand symlinks or hardlink dedup — each
# sysroot/bin/<name> below is a real, independent copy of the same toybox
# binary. That costs disk space per applet instead of one shared inode, but
# needs no changes to mkfs and keeps this Makefile self-contained.
include ../mk/port.mk
# Applets SchOS provides itself, which toybox must not link over.
#
# The two collide by name, and the collision is worse than a duplicate: the
# applets are hard links to one multicall binary, so whichever install runs
# second wins *for all 236 of them*. Installing first-party ping over the
# applet replaced the shared inode and every applet became ping; installing
# the applet over first-party ping put it back and lost the real one.
#
# SchOS's ping talks to its own lwIP stack and knows there is no resolver yet.
# toybox's would be the wrong answer even if it worked.
TOYBOX_SKIP = ping
TOYBOX_VERSION := 0.8.12
TOYBOX_URL := http://landley.net/toybox/downloads/toybox-$(TOYBOX_VERSION).tar.gz
TOYBOX_SHA256 := ad88a921133ae2231d9f2df875ec0bd42af4429145caea7d7db9e02208a6fd2e
TARBALL := $(DISTDIR)/toybox-$(TOYBOX_VERSION).tar.gz
CFLAGS := -static -D__SCHOS__ -I$(MUSL)/include -fno-stack-protector
LDFLAGS := -static -L$(MUSL)/lib -lc
# The applet list is not hard-coded any more: `toybox` with no arguments prints
# exactly the applets the binary was built with, so the install below asks the
# binary itself. A hand-maintained list installed 57 of the ~250 available and
# silently drifted every time the toybox config changed.
$(TARBALL): | $(DISTDIR)
curl -fsSL -o $@ $(TOYBOX_URL)
sh $(SHA256CHECK) "$(TOYBOX_SHA256)" "$@"
download: $(TARBALL)
$(SRCDIR): $(TARBALL)
mkdir -p $(PORTDIR)
tar -xzf $< -C $(PORTDIR)
mv $(PORTDIR)/toybox-$(TOYBOX_VERSION) $@
# Applets toybox has written but leaves out of defconfig.
#
# `defconfig` builds the 236 applets upstream considers finished. The other
# ~50 live in toys/pending/ -- complete enough to use, not yet held to toybox's
# standard for a release -- and among them are **awk**, **bc** and **diff**,
# which is a large part of why the shipped userland felt thin. They are the
# same 0BSD code from the same tarball; the only thing standing between them
# and the image was a config default.
#
# This list is curated rather than "enable everything", because everything does
# not build: `strace` wants xfork and ptrace, `syslogd` wants BSD's
# facilitynames[], and `git` wants OpenSSL. Those three break the link, so they
# are not here. If one of them ever matters, it goes in with the patch it needs
# rather than by widening a wildcard.
#
# `vi` is in pending too and is deliberately **not** on this list. SchOS ships
# an editor you do not have to be taught -- see ports/kilo.
TOYBOX_PENDING = \
AWK BC DIFF MORE EXPR TR HEXDUMP STTY XZCAT CSPLIT \
LSOF MAN LAST FDISK CHSH GETTY \
TELNET TRACEROUTE ARP ARPING ROUTE \
IPCS IPCRM USERADD USERDEL GROUPADD GROUPDEL
# toybox has no out-of-tree build mode, so its source is copied into $(BUILDDIR)
# and built in place. $(SRCDIR) is an order-only prerequisite (|): the extracted
# tarball never changes, and depending on it normally made a directory mtime
# trigger a rebuild. The rebuild then failed anyway, because `cp -r` over a
# populated $(BUILDDIR) hits "File exists" on kconfig's symlinks — so wipe it
# first and always build from a clean copy.
#
# Two things happen to the copy before it is built.
#
# **The shebangs are rewritten.** toybox's build scripts start `#!/bin/bash`,
# and on a distribution that does not keep bash at that path -- NixOS, and any
# other that takes the FHS as a suggestion -- the build fails at the first
# script with ENOENT. bash is installed and on PATH; it is only the absolute
# path that is wrong, so `/usr/bin/env bash` fixes it everywhere without
# needing bash to move. This is not a nicety: without it toybox could not be
# rebuilt on this machine at all, and the binary in the tree was one nobody
# could reproduce.
#
# **Then the pending applets are switched on**, by editing .config directly.
# Not via `make oldconfig`, which is the obvious way and does not work: every
# pending symbol is `default n`, so oldconfig dutifully turns all of them back
# off again. generated/ is removed so the edit is actually noticed -- toybox
# caches the expanded config there and will otherwise link the previous set and
# report success.
$(BUILDDIR)/toybox: | $(SRCDIR)
rm -rf $(BUILDDIR)
mkdir -p $(BUILDDIR)
cp -r $(SRCDIR)/. $(BUILDDIR)/
sed -i '1s|^\#!/bin/bash$$|\#!/usr/bin/env bash|' $(BUILDDIR)/scripts/*.sh
$(MAKE) -C $(BUILDDIR) defconfig
for c in $(TOYBOX_PENDING); do \
sed -i "s/^\# CONFIG_$$c is not set$$/CONFIG_$$c=y/" $(BUILDDIR)/.config; \
done
rm -rf $(BUILDDIR)/generated
$(MAKE) -C $(BUILDDIR) CC="$(CC)" CFLAGS="$(CFLAGS)" LDFLAGS="$(LDFLAGS)"
# Install every applet the binary provides, one name per applet.
#
# mkfs deduplicates by content, so all of these collapse to a single inode with
# many hard links in the image -- the cost is one copy, not one per name. On the
# host we try a hard link first for the same reason and fall back to a copy if
# the filesystem will not take one (NTFS via WSL usually will).
#
# The work hangs off a stamp file rather than off the phony `install` target.
# port.mk declares install .PHONY for every port, so this recipe used to run on
# every single build -- re-creating 236 links whether or not toybox had been
# rebuilt. That is slow on its own, and worse, it bumped the mtime of 236 files
# in sysroot/ every time, which made the top-level build think the staged tree
# had changed and rebuild the whole filesystem image from scratch.
install: $(BUILDDIR)/toybox.installed
$(BUILDDIR)/toybox.installed: $(BUILDDIR)/toybox | $(SYSROOT)/bin
cp $< $(SYSROOT)/bin/toybox
chmod 755 $(SYSROOT)/bin/toybox
@for a in $$($(BUILDDIR)/toybox); do \
case " $(TOYBOX_SKIP) " in *" $$a "*) continue ;; esac; \
rm -f $(SYSROOT)/bin/"$$a"; \
ln $(SYSROOT)/bin/toybox $(SYSROOT)/bin/"$$a" 2>/dev/null \
|| cp $< $(SYSROOT)/bin/"$$a"; \
done
@echo "toybox: installed $$($(BUILDDIR)/toybox | wc -w) applets"
@touch $@
clean:
rm -rf $(PORTDIR)
distclean: clean
rm -f $(TARBALL)