This is Part 1 of a series that takes a GNOME application from an empty directory to acceptance into GNOME Circle. Each post is self-contained, but the series follows a single arc — and a real app — through every stage of the journey.
Why GNOME
If you’re building a desktop Linux application in 2026, you’ve got choices. KDE Plasma has Kirigami. Elementary has Granite. You can reach for Electron, Tauri, or a dozen other cross-platform toolkits and call it a day.
I chose GNOME because of what it feels like to use. The GNOME desktop provides a clean, distraction-free approach to computing. Its consistency rivals early macOS — there’s a feeling that everything is in its place. When I started building Moments, my photo management app, that consistency carried over into the development experience in a way I didn’t expect. The GNOME Human Interface Guidelines aren’t suggestions — they’re a design language. Follow them and your app inherits a coherent visual identity, consistent interaction patterns, and accessibility support you didn’t have to design yourself. Your header bar looks right. Your adaptive layout works the way users expect. Your keyboard navigation is correct.
That matters more than it sounds like it should. Users develop muscle memory around their environment. An app that respects that muscle memory earns trust immediately. An app that doesn’t — even if it’s technically superior — creates friction. GNOME gives you a head start on eliminating that friction, but only if you actually use the toolkit the way it’s meant to be used.
The ecosystem is also unusually healthy for an open source desktop project. Flathub provides distribution. GNOME Circle provides recognition, infrastructure, and community. The tooling — Builder, Workbench, Cambalache — is actively maintained and improving. And the community, while not enormous, is engaged and welcoming in a way that larger ecosystems sometimes aren’t.
Why Rust
GNOME has traditionally been a C ecosystem. GTK is written in C. GLib is written in C. The GObject type system — which underpins everything — is C’s answer to object-oriented programming, complete with manual reference counting and a macro-heavy convention system that works remarkably well once you’ve internalised it.
You don’t need to write C to build GNOME apps. The GObject Introspection system generates bindings for dozens of languages, and three in particular have strong GNOME stories: Python with PyGObject, Vala (a language designed specifically for GObject development), and Rust with gtk-rs.
I’m writing this series in Rust, and I think it’s the right default choice for new GNOME apps in 2026.
The ecosystem has moved. Look at recent GNOME Circle apps — Fractal, Switcheroo, Hieroglyphic — the Rust cohort is growing fast. When you hit a problem building a Rust/GTK app, there’s now a meaningful body of real-world code to reference. That wasn’t true even two years ago.
The gtk-rs bindings are mature. The gtk4-rs and libadwaita-rs crates provide safe, idiomatic Rust wrappers around the full GTK4 and libadwaita API surface. They handle reference counting, type casting, and signal connections in a way that feels natural in Rust. These aren’t thin C bindings with unsafe blocks everywhere — they’re a genuine Rust API for building GTK applications.
Rust’s strengths align with desktop app needs. Memory safety without garbage collection. Fearless concurrency. A type system that catches entire categories of bugs at compile time. These aren’t just theoretical benefits — they show up in practice when you’re managing complex UI state, handling async operations, and trying to ship software that doesn’t crash.
The tooling works. rust-analyzer provides excellent IDE support. Cargo handles dependency management. The Flatpak SDK includes a full Rust toolchain. You can get from cargo init to a running Flatpak application without fighting your tools.
There are tradeoffs. Rust has a steeper learning curve than Python, and the GObject subclassing pattern in Rust adds a layer of complexity that doesn’t exist in Python or Vala. Compile times are slower. Iteration speed — especially building inside Flatpak — is meaningfully worse than Python’s edit-and-run loop. These are real costs, and I won’t pretend otherwise. But the learning curve is a curve, not a wall, and this series will walk through every part of it.
Setting up the development environment
Before you write a single line of Rust or GTK code, you need three things: GNOME Builder, the Flatpak SDK, and a working understanding of why we’re using Flatpak from day one.
Why Flatpak from the start
Most development guides have you install GTK libraries on your host system, write code against them, and worry about packaging later. This works — until it doesn’t. The failure mode is specific and painful: you develop against GTK 4.18 on your host, everything works, you go to package the app for Flathub six months later, and discover that the GNOME SDK version you need to target ships GTK 4.16, and three APIs you depend on don’t exist.
Building inside Flatpak from the beginning eliminates this entire class of problem. Your development environment matches your deployment environment. The libraries you link against are the libraries your users will have. The dependencies are explicit and reproducible. When you submit to Flathub, there are no surprises.
It’s slightly more friction up front. It’s dramatically less friction in total.
Install Flatpak and Flathub
If you’re running Fedora, Flatpak is already configured. For other distributions:
# Install Flatpak (if not present)
sudo apt install flatpak # Debian/Ubuntu
sudo pacman -S flatpak # Arch
# Add the Flathub remote
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
Install the GNOME SDK
The GNOME SDK provides the full development environment — GTK4, libadwaita, GLib, and the Rust toolchain — inside a Flatpak runtime. You need both the runtime and the SDK, plus the Rust extension:
# Install the GNOME 50 SDK and runtime
flatpak install flathub org.gnome.Sdk//50 org.gnome.Platform//50
# Install the Rust extension for the SDK
flatpak install flathub org.freedesktop.Sdk.Extension.rust-stable//25.08
A note on version numbers: the GNOME SDK version (50, at time of writing) corresponds to the GNOME release. The Rust extension version (25.08) corresponds to the freedesktop SDK version that the GNOME SDK is built on. These version numbers will change — check the Flathub runtime page for current versions.
Install GNOME Builder
GNOME Builder is the IDE purpose-built for GNOME development. It understands Meson, Flatpak manifests, and the GNOME SDK. It can build your application inside the Flatpak sandbox, run it, and provide code intelligence — all without you manually configuring build environments.
flatpak install flathub org.gnome.Builder
Builder isn’t the only option. You can use VS Code, Neovim, or any editor you prefer — I’ll cover that setup in a later post on developer experience. But Builder eliminates the most configuration friction for getting started, and it’s what I’d recommend for your first project.
Verify the toolchain
Open a terminal inside the Flatpak SDK environment to confirm everything is in place:
# Enter the SDK environment
flatpak run --command=bash org.gnome.Sdk//50
# Inside the SDK shell:
rustc --version
cargo --version
pkg-config --libs gtk4
pkg-config --libs libadwaita-1
You should see a recent stable Rust version and successful pkg-config output for both GTK4 and libadwaita. If any of these fail, the SDK or Rust extension didn’t install correctly — reinstall them before continuing.
A note on host development
You can also develop on your host system by installing the GTK4 and libadwaita development packages directly:
# Fedora
sudo dnf install gtk4-devel libadwaita-devel
# Arch
sudo pacman -S gtk4 libadwaita
# Ubuntu/Debian (may lag behind on versions)
sudo apt install libgtk-4-dev libadwaita-1-dev
This gives you faster compile times and a tighter edit-compile-run loop, which matters during active development. Many Rust/GTK developers work this way day-to-day, using host-installed libraries for rapid iteration and Flatpak builds for testing and release.
The risk is version drift between your host libraries and the Flatpak SDK. If you go this route, check which GTK4 and libadwaita versions your host provides and compare them to the GNOME SDK you’re targeting. As long as your host version is equal to or newer than the SDK version, you’re fine. If it’s older, you’ll hit missing APIs.
My recommendation: start with Flatpak-only builds until you’ve got a working app, then add host builds as an optimisation once you understand the version boundaries.
The tools you will be using
Before we start writing code in Part 2, here’s a brief orientation to the tools that’ll appear throughout this series. You don’t need deep knowledge of any of them yet — just awareness that they exist and what role they play.
Meson — The build system. GNOME apps use Meson, not Cargo, as the top-level build system. Cargo still builds your Rust code, but Meson orchestrates everything else: compiling GResources, installing desktop files, generating application metadata, and invoking Cargo as part of the build. Part 3 covers Meson in detail.
Blueprint — A markup language for defining GTK user interfaces. Blueprint compiles to the XML that GTK’s GtkBuilder expects, but it’s dramatically more readable. You can also build your entire UI in Rust code without Blueprint — we’ll cover both approaches.
GResource — GNOME’s system for bundling assets (UI templates, icons, CSS) into your application binary. Instead of loading files from disk at runtime, GResources get compiled into the binary and accessed by path. It’s how GNOME apps ship self-contained.
Appstream — The metadata standard that app stores (including Flathub) use to display your app’s name, description, screenshots, and release notes. You’ll need to get this right for Flathub acceptance — Part 9 covers it.
Workbench — A playground app for experimenting with GTK widgets, CSS, and Blueprint templates. If you want to test a UI idea without rebuilding your whole app, Workbench is where you do it. Grab it from Flathub.
What we’re building
Part 2 opens with introducing the app we’ll build throughout this series. It won’t be a toy counter or a TODO list — it’ll be something with enough complexity to hit real architectural decisions, real packaging challenges, and real Flathub review feedback.
Every post will be self-contained enough that you can follow along building your own app. The problems we’ll solve — state management, async operations, data persistence, adaptive layouts, packaging — are universal to any non-trivial GTK application. We’re documenting the actual experience of taking a GNOME app from zero to Circle, including the parts that aren’t in any documentation.
What comes next
Before we build a window, we need to understand the type system underneath it. Every widget, every signal, every property binding in GTK is built on GObject — and the way GObject maps to Rust is the single biggest conceptual hurdle for developers coming from other Rust backgrounds.
In Part 2, we’ll build a simple data model from scratch and use it to understand GObject’s inner/outer type pattern, properties, signals, and property bindings. No GTK widgets yet — just the foundation that makes everything else click once we start building the actual app in Part 3.