WTF — Wayland Tiling, F# Home Releases GitHub

WTF NativeAOT build

WTF builds two ways. The default (JIT) build is unchanged and ships every feature. The AOT build is an additive -p:WtfAot=true flavor that trades the reflection/JIT-only subsystems for a small, fast-starting native binary.

Feature matrix

Capability JIT build (default) AOT build (-p:WtfAot=true)
Core tiling WM (layouts, workspaces, keybinds, undo/redo, sessions) yes yes
Wallpaper (solid + image) yes (ImageSharp) yes (ImageSharp)
C shim / wlroots interop yes yes
Agent control socket (query / act / tools) yes yes
{"notify"} socket verb yes (D-Bus notification) degraded (no-op "notified")
config.fsx hot-reload + live REPL {"eval"} (FCS) yes NO — built-in default config only
Reflective layout plugins (~/.config/wtf/plugins) yes NO — built-in layouts only
D-Bus desktop shell (notifications, battery, network, MPRIS media keys) yes NO
In-process LLM agent ({"ask"}, Anthropic) yes NO ("agent disabled (AOT build)")
Startup / runtime self-contained .NET (~76 MB), JIT warmup native binary, fast start, low mem
Reconfigure edit config.fsx, hot-reloads live recompile the binary (xMonad-style)

The AOT build drops FSharp.Compiler.Service, Tmds.DBus, Microsoft.Extensions.AI and Anthropic.SDK from the binary because they rely on JIT / reflection-emit / dynamic-code that NativeAOT cannot include. The configuration tradeoff mirrors xMonad: the JIT build is xmonad.hs you reload live; the AOT build is a statically compiled WM you rebuild to reconfigure.

How the flavors are wired

WTF_NO_FCS and WTF_NO_PLUGINS reuse the existing IConfigEngine/NullConfigEngine and IPluginLoader/NullPluginLoader seams; WTF_NO_DESKTOP/WTF_NO_AGENT are new host-only symbols (the host references Desktop/Agent directly).

Build the lean graph (no clang needed)

export DOTNET_ROOT="$HOME/.dotnet" PATH="$HOME/.dotnet:$PATH"
dotnet build src/WTF.Host/WTF.Host.fsproj -c Release -p:WtfAot=true

This compiles the exact AOT dependency closure (Core + lean Host + FSharp.Core + ImageSharp — verify with ls src/WTF.Host/bin/Release/net10.0/*.dll) and runs the gating. It is the verifiable proof the code is AOT-shaped. It does NOT invoke ILC/clang.

Produce the native binary (needs clang)

NativeAOT links the native image with clang on Linux (gcc is not sufficient). On Debian/Ubuntu:

sudo apt install clang zlib1g-dev
bash scripts/aot-publish.sh        # publishes + prints the real binary size

Without clang the script prints the install hint and stops; nothing else is blocked. The script never fabricates a size — it prints du -h of the binary only after a real publish.

Analyzer / readiness note (honest)

IsAotCompatible enables the Roslyn ILLink trim/AOT analyzers, but those analyzers only attach to C# compilations. WTF.Core and WTF.Host are F#, so dotnet build -p:WtfAot=true emits no IL2xxx/IL3xxx warnings — the analyzer signal the flag normally gives is not available at build time for this codebase. The real IL-level AOT warnings come from ILC during dotnet publish -p:PublishAot=true, which runs before the clang link step (so it needs clang to complete). The verifiable readiness signal without clang is therefore: (1) the lean graph compiles, (2) the reflection packages are absent from the output closure, (3) WTF.Core is pure (no Reflection.Emit / Assembly.Load / dynamic; JSON via the reflection-free System.Text.Json.Nodes API). IsAotCompatible is still set on Core: it propagates IsTrimmable, documents intent, and makes the eventual publish treat ILC warnings as build-relevant.

Callback interop note

The six C->F# compositor callbacks (onViewMap/onViewUnmap/onKey/onOutputResize/ onReady/onDrain) use rooted concrete delegates + Marshal.GetFunctionPointerForDelegate (Program.fs). This is NativeAOT-compatible: ILC synthesizes the reverse-pinvoke marshalling thunk at compile time because every delegate type is statically known and instantiated (no dynamic code → no IL3050), and all signatures are blittable (int/uint32/nativeint/double; no string/array marshalling in the ABI). The delegates are GC-rooted for the whole run (let d* = … across wtf_run + GC.KeepAlive). [<UnmanagedCallersOnly>] would shave one thunk per event but is fragile in F# (no clean &Method / delegate*<…> managed-function-pointer syntax), so the delegate approach is kept by design. Finally confirmed by the ILC pass during PublishAot.

Found a problem on this page? Edit it on GitHub — the site rebuilds from docs/ automatically.