# slosh in a browser tab. # # Bellard's TinyEMU compiled to wasm boots a riscv64 Linux; we hand it a root # filesystem with a cross-compiled slosh in it. `make` builds the whole thing # into build/site, `make serve` puts it on a port. # # Run this from web/demo (or `make -C web/demo`), not from the # repository root: it is a separate build with a separate target triple and it # deliberately shares nothing with the top-level one but the source files. ROOT := ../.. ZIG ?= $(HOME)/zig-0.16.0/zig CC := $(ZIG) cc TARGET := riscv64-linux-musl VT := $(ROOT)/vendor/libghostty-vt VT_OUT := build/vt VT_LIB := $(VT_OUT)/lib/libghostty-vt.a VT_INC := $(VT_OUT)/include SRC := $(wildcard $(ROOT)/src/*.c) OBJ := $(patsubst $(ROOT)/src/%.c,build/obj/%.o,$(SRC)) DEPS := $(OBJ:.o=.d) GUEST := build/slosh-riscv64 # -Os, no -g: every byte of this binary is pulled through a virtio block device # by somebody's browser, 256 KiB at a time. CFLAGS := -target $(TARGET) -std=c23 -Os -Wall -Wextra -Wno-unused-parameter \ -MMD -MP -I$(VT_INC) -I$(ROOT)/include -Ibuild # The same derivation as every other artifact (contrib/version: the tag is # the single source of truth), so the demo cannot fall behind a release the # way a hand-written number here once did. +webdemo, because this binary is # the riscv64 guest build and should say so when someone asks it. VERSION := $(shell cd $(ROOT) && contrib/version) SITE := build/site IMAGE := build/rootfs.bin IMAGE_MB ?= 48 PORT ?= 8000 # What the browser downloads, beyond our own page. The emulator comes from # emulator/dist -- ours, built from Bellard's source -- not from vendor/: # his prebuilt blob wedges on disks larger than 16 MiB (see emulator/). VENDOR_SITE := bbl64.bin kernel-riscv64.bin xterm.js xterm.css EMU_SITE := emulator/dist/riscvemu64-wasm.js emulator/dist/riscvemu64-wasm.wasm WEB_SITE := index.html demo.js demo.css slosh.cfg V ?= 0 ifeq ($(V),0) Q := @ say = @printf ' %-5s %s\n' '$(1)' '$(2)' else Q := say = @: endif .PHONY: all vendor guest rootfs site serve check clean distclean help .DEFAULT_GOAL := help help: ## show this @grep -hE '^[a-z-]+:.*##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t14 @printf '\n%-13s %s\n' 'PORT=8080' 'which port `make serve` uses' @printf '%-13s %s\n' 'IMAGE_MB=64' 'how big the guest filesystem is' all: site ## build everything into build/site # ── the third-party half ──────────────────────────────────────────────────── vendor: vendor/.stamp ## download the emulator, kernel, base image and xterm.js vendor/.stamp: fetch-vendor $(Q)./fetch-vendor @touch $@ # ── slosh, for a machine nobody here owns ───────────────────────────────── # # -Dsimd=false is not a size choice: Google Highway, which libghostty-vt uses # for SIMD, does not compile for rv64gc -- it emits RVV intrinsics for a vector # extension this target does not have. The scalar paths are the same code # without the hand-vectorised fast path, which for a demo VM is free. # -Dversion-string, as at the top level: the vendored tree has no .git, so # upstream's version detection runs `git describe` in *our* repository -- and # on a tag checkout (exactly where web/publish builds a release site) it # finds a slosh tag, which upstream treats as a fatal mismatch. The manifest # prerequisite is the top level's too: a vendor bump must rebuild this lib, # or the demo compiles new code against the old headers. Both stayed hidden # behind a stale build/vt until v0.1.7 cleared it. VT_VERSION := $(shell sed -n 's/^[[:space:]]*\.version = "\([^"]*\)".*/\1/p' $(VT)/build.zig.zon | head -1) $(VT_LIB): $(VT).vendor.json $(call say,[ZIG],libghostty-vt $(TARGET)) $(Q)cd $(VT) && PATH="$(dir $(ZIG)):$$PATH" zig build \ -Demit-lib-vt -Dsimd=false -Doptimize=ReleaseFast -Dtarget=$(TARGET) \ -Dversion-string=$(VT_VERSION) \ --prefix $(abspath $(VT_OUT)) --cache-dir $(abspath build/vt-cache) # FORCE, as at the top level: rewritten only when the value actually changes, # so a new commit rebuilds and `make` twice in a row does not. .PHONY: FORCE build/version.h: FORCE | build @printf '/* generated by web/demo/Makefile */\n#define SLOSH_VERSION "%s+webdemo"\n' \ '$(VERSION)' > $@.tmp @cmp -s $@.tmp $@ || mv $@.tmp $@ @rm -f $@.tmp build build/obj: @mkdir -p build/obj # The logo, exactly as the top-level Makefile bakes it: logo.txt is the # source of truth and the splash includes this. build/logo.h: $(ROOT)/logo.txt | build $(call say,[GEN],$@) $(Q){ printf '/* generated from logo.txt; edit that, not this */\n'; \ printf 'static const char *const LOGO[] = {\n'; \ sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g' -e 's/^/ "/' -e 's/$$/",/' $(ROOT)/logo.txt; \ printf '};\n'; } > $@ # The embedded terminfo entry, exactly as the top-level Makefile bakes it: # pty.c includes it, and this Makefile compiles pty.c with its own rules. build/terminfo.h: $(ROOT)/contrib/terminfo/xterm-ghostty.terminfo | build $(call say,[GEN],$@) $(Q){ printf '/* generated from contrib/terminfo/xterm-ghostty.terminfo */\n'; \ printf 'static const unsigned char TERMINFO_GHOSTTY[] = {\n'; \ od -An -v -tx1 $< | sed -e 's/ \([0-9a-f][0-9a-f]\)/0x\1,/g'; \ printf '};\n'; } > $@ build/obj/%.o: $(ROOT)/src/%.c build/version.h build/logo.h build/terminfo.h $(VT_LIB) | build/obj $(call say,[CC],$(notdir $<)) $(Q)$(CC) $(CFLAGS) -c $< -o $@ # stb_image is third-party and does not build clean under our warnings. build/obj/png.o: $(ROOT)/src/png.c build/version.h $(VT_LIB) | build/obj $(call say,[CC],$(notdir $<)) $(Q)$(CC) $(CFLAGS) -I$(VT)/src/stb -Wno-unused-function -c $< -o $@ # Static, because the guest has glibc 2.24 and we built against musl. It also # means the binary is the only thing that has to land in the image. $(GUEST): $(OBJ) $(VT_LIB) $(call say,[LD],$@) $(Q)$(CC) -target $(TARGET) -Os $(OBJ) $(VT_LIB) -o $@ -s guest: $(GUEST) ## cross-compile slosh for riscv64 # ── the guest filesystem ──────────────────────────────────────────────────── # # The kernel we use has no initramfs support (CONFIG_BLK_DEV_INITRD is off: it # ignores an initrd and panics for want of a root), so the root filesystem is an # ext2 image on a virtio disk, split into 256 KiB blocks that the emulator # fetches over HTTP as the guest touches them. A visitor downloads the parts of # the image they use, not all of it. rootfs: $(IMAGE) ## build and split the guest filesystem # find -L: overlay contains a symlink (the theme, pointing into contrib/themes) # and -type f without -L skips symlinks -- editing the theme would not rebuild # the image. With -L the prereq is the symlink path, and make stats through it, # so the target's mtime is the one compared. $(IMAGE): $(GUEST) build-rootfs vendor/.stamp $(shell find -L overlay -type f 2>/dev/null) $(Q)./build-rootfs --base vendor/alpine-minirootfs-riscv64.tar.gz --slosh $(GUEST) \ --apks vendor/apks --overlay overlay --out $@ --split $(SITE)/rootfs --size $(IMAGE_MB)M # Everything except the page can be checked without a browser: same emulator, # same kernel, same image, driven over a pty. `make check` before you open # devtools, always. check: $(IMAGE) ## boot the image in a native emulator and check slosh runs $(Q)./boot-check # ── the page ──────────────────────────────────────────────────────────────── site: $(IMAGE) ## assemble build/site @mkdir -p $(SITE) $(Q)cp $(addprefix web/,$(WEB_SITE)) $(SITE)/ $(Q)cp $(addprefix vendor/,$(VENDOR_SITE)) $(SITE)/ $(Q)cp $(EMU_SITE) $(SITE)/ $(Q)cp vendor/TINYEMU-MIT-LICENSE.txt $(SITE)/ $(Q)for f in kernel-riscv64.bin bbl64.bin riscvemu64-wasm.js riscvemu64-wasm.wasm \ xterm.js xterm.css demo.js demo.css; do \ gzip -9 -n -c $(SITE)/$$f > $(SITE)/$$f.gz; \ done @printf ' site %s (%s; %s gzipped)\n' "$(SITE)" \ "$$(du -sh $(SITE) | cut -f1)" \ "$$(du -shc $(SITE)/*.gz $(SITE)/rootfs/*.gz 2>/dev/null | tail -1 | cut -f1)" serve: site ## serve build/site on http://localhost:$(PORT) ./serve --dir $(SITE) --port $(PORT) clean: ## remove build/ (keeps the downloads) rm -rf build distclean: clean ## also remove vendor/ rm -rf vendor -include $(DEPS)