# webdemo
slosh in a browser tab: a riscv64 Linux, booted by Fabrice Bellard's TinyEMU
compiled to WebAssembly, with a root filesystem we build and slosh running as
the thing you land in. Nothing is installed, nothing is uploaded, and reloading
the page throws the machine away.
```sh
cd web/demo
make all # download, cross-compile, build the filesystem, assemble the site
make serve # http://localhost:8000
```
A bare `make` prints the target list, as it does at the top level.
`make serve` is a static file server. The output in `build/site` is *only*
static files, so anything that serves a directory will do -- including GitHub
Pages, S3, or `python3 -m http.server` -- with one condition: `.wasm` must be
served as `application/wasm` or the browser will not instantiate it. That is
the single thing our `serve` does that a default configuration might not.
It cannot be opened from `file://`: the emulator fetches the VM config and the
disk blocks with XMLHttpRequest.
## What you need
| | |
|---|---|
| zig 0.16 | `$HOME/zig-0.16.0/zig`, or `make ZIG=/path/to/zig` -- the same one the top-level build wants |
| e2fsprogs | `mke2fs`, `debugfs`, `dumpe2fs` -- all used unprivileged |
| curl | for `fetch-vendor` |
| gcc | only for `make check`, which builds the native emulator |
No npm, no bundler, no emscripten, and no root at any point.
## The targets
| | |
|---|---|
| `make vendor` | download the emulator, kernel, Alpine rootfs and xterm.js into `vendor/`, each pinned by sha256 |
| `make guest` | cross-compile slosh to a static riscv64 binary |
| `make rootfs` | build the guest filesystem and cut it into HTTP blocks |
| `make site` | assemble `build/site` |
| `make check` | boot the image in a native TinyEMU and check slosh comes up -- no browser |
| `make serve` | serve it (`PORT=8080` to move it) |
| `make clean` | remove `build/`; `distclean` also removes the downloads |
`./boot-check --interactive` boots the same image in your terminal and hands
you the keyboard, which is the fastest way to try a change to the overlay.
`C-a x` quits the emulator -- note that this is also slosh's leader key, so
type it twice if the session swallows the first.
## How it fits together
```mermaid
flowchart LR
A[src/*.c + libghostty-vt] -->|zig cc riscv64-linux-musl| B[static slosh]
C[Alpine minirootfs riscv64] --> D[build-rootfs]
E[overlay/] --> D
B --> D
D -->|mke2fs -d| F[rootfs.bin]
F -->|256 KiB blocks| G[build/site/rootfs/]
H[riscvemu64-wasm] --> I[browser]
G -->|fetched lazily over HTTP| I
J[web/ + xterm.js] --> I
```
Four decisions are worth knowing about, because each one was a dead end
first.
**riscv64, not x86.** The x86 emulator in the JSLinux tarball is a prebuilt
blob with no source in TinyEMU's release -- `x86_cpu.c` there is a 96-line KVM
shim, not a software core -- and both its kernel and its userland are 32-bit
i386. riscv64 is the only target with published source, a 64-bit userland, and
a toolchain we already have: zig cross-compiles to `riscv64-linux-musl` out of
the box.
**A disk, not an initramfs.** The guest kernel is Bellard's prebuilt Linux
4.15, and it is built without `CONFIG_BLK_DEV_INITRD`: an `initrd:` line in the
VM config is accepted, ignored, and followed by a panic for want of a root.
The root filesystem is therefore an ext2 image on a virtio disk, split into
256 KiB files fetched over plain HTTP. `blk.txt` lists every block in its
`prefetch:` array, so the whole disk streams in at startup while boot
proceeds -- boot-critical blocks sit at the front, a demand read of a block
already in flight joins it, and once the stream ends the session never
touches the network again. A cold block fetched mid-keystroke reads as the
demo hanging, which is worse than the ~11 MiB the stream costs. An
initramfs would buy the same bytes-up-front without the overlap: nothing
boots until all of it has arrived.
Compression happens on the wire: the build writes a `.gz` twin beside every
block and every big asset, `serve` answers `Accept-Encoding: gzip` with the
twin and `Content-Encoding: gzip`, and XHR decodes before the emulator looks.
The emulator neither knows nor cares. Any competent static host or CDN does
the equivalent on its own.
**xterm.js, not the term.js in the tarball.** Bellard's terminal has no
truecolor and no wide-character handling. slosh paints 24-bit colour shaders
and lays out CJK, so that terminal would misrepresent both. The emulator
expects a global `term` with `write` and `getSize`; `web/demo.js` provides those
two methods over xterm.js and nothing else.
**Our emulator build, not Bellard's blob.** The prebuilt
`riscvemu64-wasm.js` in the JSLinux tarball does not match the source
released beside it: past 16 MiB into the disk it stops issuing block
fetches, so the first command to stat an inode in a later block group --
`ls -la`, typically -- wedges or segfaults in the guest. His demo images are
4 MiB, so upstream never crossed the line; our 32 MiB image did. The same
source compiled natively serves the same blocks flawlessly, so
`emulator/build-emulator` compiles the emulator from the tarball's own
source (ported to a current emscripten;
`emulator/patches/0001-modern-emscripten.patch` is the whole port) and the
result is committed in `emulator/dist/` -- because it needs the emscripten
SDK to rebuild, and nothing else here does. It is also half the size of the
blob it replaces.
## The guest
Alpine's riscv64 minirootfs -- musl, busybox, `apk`, `less`, `top` -- plus a
real vim with syntax highlighting (the `vim` package from pinned `.apk`s, its
26 MiB runtime pruned to the ~10 that highlighting uses: `syntax/`, filetype
detection, colour schemes; no docs, no spell, no netrw), plus `overlay/`,
which is the whole of what makes it ours:
| | |
|---|---|
| `etc/inittab` | four lines: run `rcS`, then respawn slosh on the console |
| `etc/init.d/rcS` | mounts, and `devpts` in particular: no devpts, no panes |
| `etc/profile` | the environment every pane inherits, `COLORTERM` included |
| `etc/motd` | five lines, shown in a pane rather than on the console |
| `root/.config/slosh/config.kdl` | the demo's config: `theme_name "sl0p"`, and the knobs worth uncommenting |
| `root/.config/slosh/themes/` | every theme the repository ships, so `C-a t` has a list to offer |
| `root/.vimrc` | syntax on, truecolor, and nothing that wants the pruned parts |
| `root/demo.layout` | the three panes you land in |
The themes are symlinks into `contrib/themes/`, so the demo cannot drift from
the palettes the repository ships; `build-rootfs` copies them by content,
because a symlink in the image would dangle. They live where a person's own
would (`~/.config/slosh/themes`), which is also the directory `theme_name` and
the `C-a t` picker look in first -- the guest binary sits in `/usr/bin` with no
`share/slosh` beside it, so that directory is the whole list, and `boot-check`
asks the guest for it rather than trusting that.
Editing anything in `overlay/` and running `make` rebuilds the image. There is
no state to clear: the emulator's writes live in the browser's memory and die
with the tab.
## Known limits
- **The terminal size is fixed at boot.** TinyEMU sends the console size once,
from `console_init()`, and exports no way to send another. The page therefore
measures the viewport once, tells the guest, and afterwards scales the fixed
grid with a CSS transform instead of reflowing it. `?cols=`/`?rows=` override
the choice.
- **No network.** There is no `eth0` in the VM config, so `apk add` cannot
reach a repository. Everything in the demo has to be in the image.
- **Terminfo is an alias.** The image's only terminfo entries are ncurses'
base set, with `xterm-ghostty` (the `TERM` slosh sets in panes, src/pty.c)
symlinked to `xterm-256color` -- the honest subset. Truecolor still happens:
`COLORTERM=truecolor` is exported and vim's `termguicolors` is on.
- **It is an emulator.** A keystroke costs an interpreted instruction stream;
the session is responsive but the machine is not fast, and a `make` inside it
would take a while.
## Provenance
Nothing in `vendor/` is committed. `fetch-vendor` downloads each piece and
checks it against a sha256 recorded in the script, tarball and extracted member
both:
- TinyEMU / JSLinux -- BIOS, kernel, and the emulator *source*. MIT, Fabrice
Bellard, . The licence text is copied into
`build/site` next to what it covers.
- Alpine Linux riscv64 minirootfs, .
- xterm.js 5.5.0, MIT.
The one binary this directory commits is `emulator/dist/riscvemu64-wasm.{js,wasm}`
-- MIT, built from the pinned TinyEMU tarball by `emulator/build-emulator`,
reproducible from the patch beside it. The kernel and the Alpine userland are
GPL binaries; a repository that shipped those would owe their source, so we
point at their publishers instead.