Publishing with Native AOT
A bibui app on the Wayland host and the managed rasterizer publishes as one native executable: no .NET runtime to install, no JIT at start-up, and no Skia, GLFW or OpenGL inside it. This page covers how to set that up, the rules your own code has to keep for the publish to stay clean, and what to expect of the result.
Which route publishes
| Route | Packages | Native AOT |
|---|---|---|
| Wayland + managed raster | Bibui.Core, Bibui.Platform.Wayland, Bibui.Raster.Managed.Backend | Yes. Tested in CI with warnings as errors |
| Silk + Skia | Bibui.Core, Bibui.Platform.Silk, Bibui.Raster.Skia | No, JIT only for now |
| Browser + managed raster | Bibui.Core, Bibui.Platform.Browser, Bibui.Raster.Managed.Backend | Not Native AOT: WebAssembly, interpreted or ahead-of-time compiled (bibui in the browser) |
The Silk + Skia route is JIT-only for now. bibui's own Silk window and Skia renderer are AOT compatible, but Silk.NET itself is not trim-safe: it finds its platform by reflection and resolves native library paths through Assembly.Location, so an AOT publish reports Silk.NET's own warnings (IL2072, IL3000, IL3002). Those cannot be suppressed for one assembly, and bibui does not ask you to accept them. Run the Silk + Skia route on the JIT, and use the Wayland + managed route when you need a native binary. This may change if Silk.NET becomes trim-safe or GPU rendering comes to the Wayland host.
The browser route is not a native binary: it is a .NET WebAssembly app, run by the .NET runtime's interpreter in the page, or compiled to WebAssembly ahead of time with -p:RunAOTCompilation=true. Its publish trims the app as a Native AOT publish does, so the same rules for your own code apply to it.
Your build code does not depend on the route. Only Program.cs, which opens the window and binds the renderer, differs.
Set up the project
You need the .NET 10 SDK and, for the native link step, clang and the zlib development headers (zlib1g-dev on Debian and Ubuntu). The app needs these libraries at run time: libwayland-client, libxkbcommon, FreeType and HarfBuzz (libfreetype6 and libharfbuzz0b on Debian and Ubuntu).
dotnet new console -n Counter && cd Counter
dotnet add package Bibui.Core
dotnet add package Bibui.Platform.Wayland
dotnet add package Bibui.Raster.Managed.BackendIn the .csproj, turn on AOT and make every trimming warning an error, so a problem fails the build instead of a start-up:
<PropertyGroup>
<PublishAot>true</PublishAot>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>PublishAot also turns on the trimming and AOT analyzers for an ordinary dotnet build, so you see the warnings while you work, not only when you publish. InvariantGlobalization is optional. It leaves out the ICU culture data, which a UI that formats its numbers with the invariant culture does not need.
Name the renderer
Open the window and construct the renderer yourself, bound to the window's memory:
using Bibui.Core;
using Bibui.Platform.Wayland;
using Bibui.Raster.Managed.Backend;
using Bibui.Core.Runtime;
using Bibui.Core.Runtime.Platform;
var runtime = BibuiRuntime.Create();
int count = 0;
void Build()
{
var page = runtime.BeginParent(runtime.Column().Sized(Size.Fill, Size.Fill));
page.Style.Padding = EdgeInsets.All(24f);
runtime.Text($"Clicked {count} times");
runtime.Gap(12f);
if (runtime.Button("add", "Add one").Clicked) count++;
runtime.EndParent(page);
}
if (!WaylandWindow.IsSupported)
{
Console.Error.WriteLine("This app needs a Wayland session.");
return 1;
}
using var window = WaylandWindow.Open(runtime, new PlatformWindowOptions("Counter", 360, 160),
new WaylandHostOptions { AppId = "dev.example.counter" });
using var managed = new ManagedRenderer(window.Cpu);
while (WaylandWindow.BeginFrame(window, runtime))
{
runtime.BeginBuild(window.Events);
Build();
runtime.EndBuild();
WaylandWindow.BeginDraw(window);
managed.Draw(runtime.DrawList);
WaylandWindow.EndDraw(window);
WaylandWindow.EndFrame(window, runtime);
}
return 0;There is no default renderer: the runtime only records each frame into a DrawList, and the renderer that draws it is one you construct (new ManagedRenderer(window.Cpu) or new SkiaCpuRenderer(window.Cpu)), so the trimmer sees the type you name and keeps it.
The window is chosen the same way: the app opens the one it names. bibui looks nothing up at run time: no platform discovery, no plug-in loading, no reflection over your types.
Publish
dotnet publish -c Release -r linux-x64 -o out
./out/Counter-p:PublishAot=true on the command line does the same, if you would rather not set it in the project. The result is a single executable. WaylandWindow.IsSupported is false outside a Wayland session (no WAYLAND_DISPLAY, or no libwayland-client), so the app can say so instead of failing to connect.
What to expect
samples/showcase, an observatory console that uses most of bibui (charts, Markdown, dialogs, theme files, a seeded theme), is the reference:
| Showcase, Wayland + managed, Native AOT | |
|---|---|
| Executable | about 5.2 MB, one file |
| First rendered frame (1440 × 900), from spawn | about 29 ms |
The app above and the getting-started app publish to about 5 MB as well. Most of that is bibui and the managed rasterizer, so it grows slowly with your own code; System.Text.Json, for example, adds about half a megabyte.
The showcase builds both routes from one project. A build property leaves Silk and Skia out of the AOT binary, so only the managed route ships:
<ItemGroup>
<PackageReference Include="Bibui.Core" Version="1.0.0" />
<PackageReference Include="Bibui.Platform.Wayland" Version="1.0.0" />
<PackageReference Include="Bibui.Raster.Managed.Backend" Version="1.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(AppRoute)' != 'wayland'">
<PackageReference Include="Bibui.Platform.Silk" Version="1.0.0" />
<PackageReference Include="Bibui.Raster.Skia" Version="1.0.0" />
</ItemGroup>
<PropertyGroup Condition="'$(AppRoute)' == 'wayland'">
<DefineConstants>$(DefineConstants);WAYLAND_ONLY</DefineConstants>
</PropertyGroup>dotnet run # JIT, Silk + Skia available
dotnet publish -c Release -r linux-x64 -p:PublishAot=true -p:AppRoute=wayland -o outCode that names SilkWindow or SkiaGlRenderer goes inside #if !WAYLAND_ONLY.
Trimming rules for your code
bibui keeps these rules in all its libraries, and an AOT app has to keep them in its own code too. Each one shows up as an IL2xxx or IL3xxx warning when it is broken; with TreatWarningsAsErrors that warning fails the build.
No reflection-based serialization.
JsonSerializer.Serialize(value)andDeserialize<T>(json)without a type-info argument read your types by reflection, which AOT cannot keep. Use a source-generated context:using System.Text.Json; using System.Text.Json.Serialization; var settings = JsonSerializer.Deserialize(json, AppJson.Default.Settings); [JsonSerializable(typeof(Settings))] partial class AppJson : JsonSerializerContext;Or read the JSON as a DOM (
JsonDocument,JsonNode), as bibui's own theme loader does. Theme files (ThemeRegistry.RegisterFile) are therefore safe to load in an AOT app.No discovery by name.
Type.GetType("..."),Activator.CreateInstanceon a computed type,Assembly.Loadand scanning assemblies for types that implement an interface all depend on code the trimmer could not see being used. Name the types in code: aswitchthat constructs each one, or a list of factories.No runtime code generation. No
System.Reflection.Emit, noExpression.Compile(), nodynamic. AOT has no JIT to run what they produce.No
Assembly.Location. It is empty in a single-file binary. Find files that ship beside the executable withAppContext.BaseDirectory, and copy them there withCopyToPublishDirectory:<None Include="themes/*.json" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />Check every dependency. A package you add can break these rules on your behalf. Publish with AOT once after adding one. A library that ships
IsAotCompatiblehas had its own code checked by its authors, not necessarily its dependencies': the Silk-basedBibui.Apppackages, andBibui.Platform.Silkunder them, are AOT-clean themselves but bring Silk.NET, which is not, so they run on the JIT.Do not suppress the warnings. An
[UnconditionalSuppressMessage]or aNoWarnfor IL2xxx/IL3xxx hides a failure that shows up later, at run time, as a missing member. Fix the code instead.
Checking a publish in CI
Headless machines have no Wayland session, so check the binary without a window. Give the app a flag that builds one frame, before any window opens, and exits:
if (args.Contains("--first-frame"))
{
runtime.BeginBuild();
Build();
runtime.EndBuild(); // lays out one frame into runtime.DrawList; throws if it fails
return 0;
}Drawing is the renderer's, not the runtime's. To exercise the rasterizer as well, draw runtime.DrawList into a buffer before returning: FramePixels.Bgra(runtime, new ManagedRenderer().Draw, out _, out _).
Then publish with warnings as errors and run it:
dotnet publish -c Release -r linux-x64 -p:PublishAot=true -p:TreatWarningsAsErrors=true -o out
./out/Counter --first-framebibui's own CI does the same for its Wayland sample.