Toolkit on Kandelo — the C/C++ library stack behind foot, Waybar and mako - #1318
Open
mho22 wants to merge 18 commits into
Open
Toolkit on Kandelo — the C/C++ library stack behind foot, Waybar and mako#1318mho22 wants to merge 18 commits into
mho22 wants to merge 18 commits into
Conversation
This was referenced Aug 26, 2026
mho22
force-pushed
the
explore-dri-hyprland
branch
from
August 28, 2026 20:14
df918dd to
916f226
Compare
mho22
force-pushed
the
explore-dri-toolkit
branch
from
August 28, 2026 20:15
267319d to
55eea05
Compare
glib, GTK and every toolkit above them call through libffi, and wasm has no way to build a call frame at runtime: the call target's signature has to exist in the module's type section before the module is compiled. The port generates a dispatch table over the signatures the stack actually uses and reaches it through call_indirect, with static closure trampolines for the callback direction. That is why the earlier Wayland-scoped shim is not enough here — a toolkit registers callbacks of shapes the shim never enumerated.
The event loop, the type system and the I/O layer every GTK program is written against. gio is ported without its gdbus half, which arrives with the bus itself in the next step, so the dependency runs one way: glib does not need a bus to exist. Callbacks land through typed thunks over the libffi dispatch, which is what makes gobject signals work at all on this target.
A desktop notification daemon is a bus client, so the bus has to exist before mako does. The session bus runs as an ordinary process and clients reach it over a unix socket. Authentication is EXTERNAL over SO_PEERCRED: the bus asks the kernel who is on the other end of the socket rather than trusting a uid the client sends, which is the only mechanism here that cannot be spoofed by the client.
pixman is the pixel-region and compositing primitive cairo draws through; utf8proc is the grapheme segmentation a terminal needs to know what one character is. Both are leaf ports with no toolkit dependency, which is why they land before cairo.
Four libraries that together turn a string into pixels: harfbuzz shapes it, fribidi orders it bidirectionally, pango lays it out, cairo draws it. A toolkit needs all four before it can render a label, so they land as one step.
… stack foot rasterises its own glyphs rather than going through a toolkit, so it needs the font stack directly: freetype to rasterise, fontconfig to resolve a family name to a file, fcft to cache the result, tllist as fcft's list primitive.
…lient foot is a real terminal that was never written with this kernel in mind, which is the point: everything it needs has to already work, and what does not work fails at its call site rather than in code written around the gap. It found several: the libc surface gaps fixed here are the ones its build and startup exposed.
GTK is the first consumer that exercises the whole stack beneath it at once — libffi dispatch, gobject signals, the gio loop, cairo and pango for drawing, fontconfig for text. Porting it is what makes an unmodified GTK application, rather than a purpose-built client, able to run here.
Waybar is a C++ GTK application, so it brings the parts of the C++ runtime the C ports never needed: exceptions, the standard library's locale and iostream machinery, and gtkmm's binding layer over gobject. It is the bar the desktop runs as-is, which is the test that matters — a status bar that talks to the compositor over the layer-shell protocol and to the system over dbus, with nothing written for this port.
mako is the notification daemon: it owns org.freedesktop.Notifications on the session bus and draws toasts as layer-shell surfaces. basu is the sd-bus implementation it links, a systemd-free fork, which is what lets it speak the bus without pulling systemd in.
GTK and Waybar recurse far deeper than any C program the SDK had linked before — deep enough that the default shadow stack overflowed during widget construction, which surfaces as a corrupted heap rather than a clean overflow. The linker also has to honour the order inputs are given. C++ static initialisers and the archive members they pull in are order-sensitive, so reordering inputs silently drops symbols that only one member defines.
Six defects that only appear once real applications run: a threaded event loop, a bus client, a process that takes signals while parked. Signals now reach a thread parked in a host-converted epoll wait — foot reaps children with SIGCHLD while sitting in its loop, and the wait has to end for the handler to run. A park that ends to run a handler only restarts when SA_RESTART says so. A poll that times out keeps its pollfd array instead of returning the kernel's scratch, and its retry map is snapshotted so a concurrent wake cannot mutate it mid-walk. A thread's initial stack pointer is 16-byte aligned, which C++ requires and the previous alignment happened to satisfy only by luck. sendmsg and recvmsg walk every iovec rather than the first. A shared memfd mapping falls back to populate-only when it cannot be mapped shared.
Harnesses that spelled an ABI number into an expected message now derive it, or state the fixture's own pinned number where the fixture is what is being asserted against. The two poll-sigmask export calls added by the epoll signal fix each carry control scalars only — a pid, a guest tid, and a mask word read from the caller's address space through an already-bounded pointer — so each is registered as a reviewed occurrence rather than left to fail the default-deny audit.
A recipe that writes outside the directory the resolver hands it leaves its outputs where the cache cannot see them, so the next resolve rebuilds it. Pointing every recipe at the resolver's own output root is what makes a warm cache actually warm. msmtpd's build script also has to join its own cache key: a script that is an input to the package it builds must be hashed with it, or an edit to the script reuses a stale archive. The new libraries are classified as platform packages, since the sysroot exposes them to any client.
Waybar is a layer-shell client and foot asks for presentation feedback, so both protocols have to be generatable before either builds. The package already declares them as inputs; this adds the XML itself.
foot's slave execvp's the shell, and exec bytes come only from the retained kernel target — never from a host resolver callback. The smoke handed dash's bytes to `onResolveExec`, which that path does not consult, and then set `PATH=/usr/bin:/bin`. On host-FS passthrough the guest therefore walked into the developer machine's own /bin/dash, which is not a Wasm module, and the slave failed with ENOEXEC. Stage the real wasm32 dash under a private bin inside the test root and point PATH at it. The callback goes with the assumption behind it.
`scripts/ci-vitest-evidence-classes.tsv` must exactly cover the live Vitest file inventory, so `ci-run-test-suite.sh vitest exact-abi-source` refuses to enumerate while the two drift. This branch raises ABI_VERSION, which is exactly when that group runs. Classify the test files this branch adds, and drop the rows for files it no longer carries. A file is `source-only` only when its whole import closure stays clear of the binary resolver; the runner enforces that separately from the cover check. The smokes exec staged wasm, so they are `prepared-product`; the remaining unit tests import only `host/src/`.
`foot.wasm`, `waybar.wasm` and `mako.wasm` each carry an absolute host
path that no longer exists:
.../libs/.fontconfig-2.15.0-rev1-wasm32-<sha>.build-stage-34774-0/
share/fontconfig/conf.avail
That is fontconfig's `--with-templatedir`, which it derives from
`--prefix` when not told otherwise. `--prefix` is the resolver's staging
directory, deleted at publication. `waybar.wasm` carries gdk-pixbuf's
`share/locale` the same way, from `--localedir`.
Nothing breaks at runtime. The one use of `FC_TEMPLATEDIR` is
`src/fcinit.c:98`, `FcConfigParseOnly (config, FC_TEMPLATEDIR, FcFalse)`
— `complain` is false and the result is discarded. The config that
matters comes from `--with-baseconfigdir=/etc/fonts`, already a guest
path. `GNOME_LOCALEDIR` is compiled in even under `--disable-nls` and
read only when a catalogue exists.
What does break is reproducibility. The staging directory name carries
the builder's PID, so two builds of fontconfig produce different
`libfontconfig.a`, and that propagates into all three binaries. The
resolver compares the loser's receipt against the winner's and aborts
the whole local build on a mismatch — the failure ncurses already
produces. foot, waybar and mako have simply not lost the race yet.
Give both paths explicitly. fontconfig also needs
`-ffile-prefix-map=$SRC_DIR=.`: the build is out-of-tree against an
absolute srcdir, so `assert` in fcmatch.c, fcserialize.c and fcweight.c
expands `__FILE__` to the work directory.
Bump both revisions: output bytes change.
Verified by building each recipe twice into separate output roots:
fontconfig 6 files, differing: libfontconfig.la, fontconfig.pc
gdk-pixbuf 16 files, differing: libgdk_pixbuf-2.0.la, gdk-pixbuf-2.0.pc
Both `.a` files are now byte-identical and hold no build path. The `.la`
and `.pc` files differ only in their `prefix=` line, which
`rewrite_install_prefix_paths` rewrites to the canonical entry before
publication.
mho22
force-pushed
the
explore-dri-hyprland
branch
from
August 30, 2026 09:10
916f226 to
ab82ed9
Compare
mho22
force-pushed
the
explore-dri-toolkit
branch
from
August 30, 2026 09:10
e2774d9 to
00c818f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
explore-dri-hyprlandgives Kandelo a tiling Wayland compositor, but every client on it is written for Kandelo. Nothing proved the compositor was a real Wayland server rather than one that happens to satisfy its own clients.Running an unmodified upstream client is that proof, and it is not a compositor change — it is a toolkit problem.
footneeds freetype, fontconfig and fcft. Waybar needs GTK3, which needs glib, gobject, gio, cairo, pango, harfbuzz, fribidi and pixman. glib needs libffi with real closures. mako needs a D-Bus session bus. None of that existed on the wasm32 sysroot.This PR is the library stack. It was split out of #1241, which had grown to carry both the ports and the desktop built on them. The desktop now sits on top of this branch.
What changed
The C library stack, bottom up
call_indirectdispatch. Wasm has no calling convention to synthesize at runtime, so the port generates the dispatch table at build time. glib's closures depend on it.footresolvesmonospacethrough.The protocol XML the toolkit clients need
wlr-layer-shell-unstable-v1.xmlandpresentation-time.xmlare declared as build inputs bypackages/registry/wayland-protocols/build.toml. Waybar is a layer-shell client and foot asks for presentation feedback, so both belong here rather than in the desktop PR. Verified withxtask build-deps program-index-context-check.What the ports exposed underneath
bits/fenv.hfor both arches, and thelinux/input-event-codes.hentries the toolkit needs.fix(kernel/host): the runtime gaps the toolkit ports exposedchangescrates/kernel/src/{signal,syscalls,wasm_api}.rsandhost/src/{kernel-worker,worker-main}.ts, and adds six C fixtures with a host test each — poll timeout preserving its pollfds,sendmsgiovec gather, signal without restart, signal to a threaded process, thread vararg alignment, and fork with a polling thread. These are real defects reached by real software, not port-specific shims.ABI
ABI_VERSIONgoes 43 → 44, withabi/snapshot.jsonregenerated in the same change.bash scripts/check-abi-version.shexits 0 and reports "ABI_VERSION and snapshot are consistent". Because v44 has no published binary release yet, the matrix build publishesbinaries-abi-v44/when this lands.Both hosts
The kernel and host fixes in
fix(kernel/host): the runtime gaps the toolkit ports exposedtouch shared code (host/src/kernel-worker.ts) rather than one host entry point, so Node and the browser take the same path. The 25 changed files underhost/test/cover it.Testing
cargo test -p kandelo --target aarch64-apple-darwin --lib— 1665 passed, 0 failed.bash scripts/check-abi-version.sh— exit 0.scripts/run-libc-tests.sh→ 302 passed, 20 XFAIL, 0 build failures, and 1 unexpected failure:regression/raise-race. That test forks 100 children inside a signal handler; 5 waits returnECHILD, reproducibly.scripts/run-libc-tests.sh:163skips it wheneverCI=true, so it does not gate this PR, and whether it regressed or predates the stack is not yet established.scripts/run-posix-tests.sh→ 174 passed, 0 FAIL, 3 XFAIL, 2 SKIPBoth suites need
WASM_POSIX_RESOLUTION_POLICY=source-only-v1andWASM_POSIX_SOURCE_ONLY_BINARY_ROOT=<repo>/local-binaries/source-only-v1.binaries-abi-v44/index.tomldoes not exist until the toolkit tier merges, so thebinaries/provenance tier cannot be materialized locally. The full host vitest suite needs both provenance tiers resolvable at once, which only CI's prepared workspace provides — it is the gate for that suite and for the browser demos.The stack has since been rebased onto
mainat888e628d5. Two gates were re-run at the new stack tipccc7c6214(explore-dri-omarchy):bash scripts/check-abi-version.sh→ exit 0, snapshot in sync and the 43 → 44 bump consistent;scripts/ci-run-test-suite.sh vitest exact-abi-source→ 3049 passed, 0 failed, with two files failing to collect on paths #1321 moved tocrates/runtime-core/. That collection failure is amaindefect and #1337 fixes it.229 files changed, 23,037 insertions, 1,060 deletions.