aboutsummaryrefslogtreecommitdiffstats
path: root/web/demo/README.md
blob: 8e3502f713469c93c474f25f61bd149a868bec7d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# 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: the `sl0p` theme |
| `root/.vimrc` | syntax on, truecolor, and nothing that wants the pruned parts |
| `root/demo.layout` | the three panes you land in |

The theme is a symlink to `contrib/themes/sl0p.kdl`, so the demo cannot drift
from the palette the repository ships; `build-rootfs` copies it by content,
because a symlink in the image would dangle.

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, <https://bellard.org/tinyemu/>. The licence text is copied into
  `build/site` next to what it covers.
- Alpine Linux riscv64 minirootfs, <https://alpinelinux.org>.
- 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.