Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

325. A build says what it can ask the desktop

Date: 2026-09-15

Status

Accepted. Closes docs/gaps.md G32.

Context

ADR-0322 bound SDL_GetSystemTheme and gave applications Host.systemTheme(). On a GNOME/Wayland desktop set to dark, it answered empty.

The desktop was not the problem. Measured on the machine that reported it:

$ gsettings get org.gnome.desktop.interface color-scheme
'prefer-dark'

$ gdbus call --session --dest org.freedesktop.portal.Desktop \
      --object-path /org/freedesktop/portal/desktop \
      --method org.freedesktop.portal.Settings.Read org.freedesktop.appearance color-scheme
(<<uint32 1>>,)                       # 1 = prefer dark

# and, calling the toolkit's own library directly:
after SDL_Init: SDL_GetSystemTheme() = 0        # 0 = UNKNOWN

The desktop answers. The portal answers. SDL does not, and the reason is in the library rather than in the session. On Linux, SDL’s theme detection is entirely the D-Bus portal — src/core/linux/SDL_system_theme.c reads org.freedesktop.appearance color-scheme from org.freedesktop.portal.Settings and subscribes to its SettingChanged signal, and there is no second path. That file is behind SDL_USE_LIBDBUS, which is #defined from HAVE_DBUS_DBUS_H, which SDL’s CMake sets from a build-time probe:

dep_option(SDL_DBUS "Enable D-Bus support" ON "${UNIX_SYS}" OFF)
...
if(SDL_DBUS)
  pkg_search_module(DBUS dbus-1 dbus)          # headers, not the library

SDL dlopens libdbus-1.so at run time, but only if the headers were present when it was compiled. On the machines that built Goldberry’s SDL they were not:

$ grep DBUS .../build_config/SDL_build_config.h
/* #undef HAVE_DBUS_DBUS_H */

so SDL_GetSystemTheme() was a compile-time constant UNKNOWN in every copy of libgoldberry.so built that way, on every Linux desktop, however it was set.

It was not only the theme, and it was not only one machine. The same probe gates the portal file dialog and the screensaver inhibit; a second gates the input method on X11; a third, device hotplug. And the absence was structural rather than accidental: LinuxDependencies — the table checkToolchain reads — listed dbus-1 as OPTIONAL under the purpose “SDL3 desktop integration”, which was an honest description until ADR-0322 bound the call; linux.yml, which builds the published artifact inside a manylinux container, installed neither dbus-devel, systemd-devel nor ibus-devel; and LinuxDependenciesTest’s CI drift guard checked that workflow for HARD_STOP rows only, because hard stops are what CI had previously been burned by.

So four things were true at once: a -dev package was missing, no local check minded, CI did not install it, and the guard that exists to catch exactly this disagreement did not look. Nothing failed. Every build was green, and a client shipped a settings screen that told its user their desktop had no light-or-dark setting.

Decision

A build that cannot ask the desktop something stops; one that is told to build anyway says so, in the binary, for ever after.

Three parts, and the third is the one that generalises.

1. The headers are a declared dependency

dbus-1 becomes NEEDED in LinuxDependencies — the necessity that already means “SDL drops this silently, so fail here because SDL will not”, the same reading libdecor-0 and the Wayland spec get. ibus-1.0 joins the table as a new row. Each desktop-integration row also names the Capability constants a library loses without it, which is what ties the build table to what an application can read back at run time.

linux.yml installs dbus-devel, systemd-devel and ibus-devel — all three verified present in the manylinux_2_28 container — and example.yml and showcase.yml gain libibus-1.0-dev. LinuxDependenciesTest now asserts that the workflow building the published artifact installs the package behind every capability, not just the hard stops.

2. The superbuild stops rather than degrading

The CMake superbuild probes for the same pkg-config modules SDL probes for (dbus-1/dbus, ibus-1.0, libudev), sets SDL_DBUS, SDL_IBUS and SDL_LIBUDEV explicitly rather than inheriting their defaults, and raises a FATAL_ERROR naming the package — on both package managers — when D-Bus is absent. It then cross-checks its own prediction against SDL’s answer, by reading the SDL_build_config.h SDL just generated: headers present and HAVE_DBUS_DBUS_H still undefined is a build that would report a capability it does not have, which is the one outcome this record rules out.

-DGOLDBERRY_REQUIRE_PLATFORM_INTEGRATION=OFF, or -Pgoldberry.allowDegradedPlatform=true through Gradle, builds without them on purpose — one flag reaching both the toolchain check and CMake, so the two halves cannot disagree about what was asked for.

3. The library reports what it is

Set<Capability> Goldberry.capabilities();
// SYSTEM_THEME, INPUT_METHOD, DEVICE_HOTPLUG, FILE_DIALOG, SCREENSAVER_INHIBIT

The superbuild passes what it found to the shim as compile definitions; goldberry_platform_capabilities() returns them as a word; NativeCapability decodes it and :core’s Capability is what an application sees — two enums, because natives.* does not leave :natives (ADR-0174), and a switch the compiler checks between them. The five bit values are rows in the layout probe’s registry, like every other constant the bindings hard-code (ADR-0010).

The bits describe the library, not the session it is loaded into. A build that can ask reports SYSTEM_THEME even on a desktop that has no such setting, because “could not ask” and “asked and was told nothing” are different facts and only the first one is fixable. The second is what an empty Host.systemTheme() means, and the two together are what let an application say something true to its user.

And the sdl3 backend warns once at start-up when a capability it ships an API for is missing — naming the package — because a log line on the machine where it is wrong is worth more than a paragraph in a document.

Consequences

The ABI version goes to 10. A new export changes the shape of the surface, so an old libgoldberry beside new Java fails at load time with the message that already exists for it. The pair is built together everywhere it matters.

A contributor without libdbus-1-dev now has to install it, or pass a flag. That is the intended cost and it is small: one package, named in the failure, on both package managers. The alternative is the state this record exists to end, where that contributor builds a lesser library and cannot tell.

INPUT_METHOD is a Linux half-truth, deliberately. On Wayland SDL drives zwp_text_input_v3 from the compositor and needs neither IBus nor Fcitx, so a build without the bit composes text perfectly well there and not at all under X11. A build-time bit cannot express “depends on the session”, so it reports what it is — whether an X11 session would have an input method — and the backend only warns when the driver actually is X11. The javadoc says so in as many words.

FILE_DIALOG is about the portal specifically. SDL has a second path on Linux — it shells out to zenity — so a library without the bit may still open a dialog on a machine that happens to have that binary. The bit reports the half the build decides.

Empty means two things and that is accepted. Goldberry.capabilities() is empty both for a library that can do none of this and for a run with no native library at all — a Java-only test, the headless backend. Both mean do not expect these to work here, which is what a caller acts on.

This does not fix libdecor. The same audit found linux.yml installs no libdecor-devel or xkeyboard-config either, and neither package exists in the manylinux_2_28 repositories — so the published Linux artifact may have a Wayland window with no titlebar, for the same class of reason and with a harder answer. That is a separate entry, not this one, and the capability mechanism above is what it should be reported through when it is written.

Alternatives considered

Read the portal from :core. It is what SDL does, and doing it ourselves would mean Goldberry talking D-Bus to the desktop — platform integration living above the backend SPI, and a second answer to a question Host.systemTheme() already answers. Worse than none.

A run-time probe instead of a build-time constant. Asking the session whether D-Bus is reachable answers a different question: a library compiled without the support cannot use a D-Bus that is right there. What an application needs to know is what its library can do, and that is decided once, at build time.

Warn instead of stopping. A warning in a configure that prints several hundred lines is a warning nobody reads — this one was effectively printed for months by SDL itself, in the form of a #undef in a generated header. The default is a stop precisely because the failure mode is silence.

One Capability.DESKTOP_INTEGRATION. Fewer constants, and it would collapse three independently-gated features into one bit that is false when any of them is, telling an application less than it needs to say anything useful.