Skip to content

Commit 912f5a1

Browse files
committed
choosing a desktop installed seventy packages nobody had written down
`desktop = "niri"` lands about seventy packages a declaration never names, and until now the only way to see them was `kuma generate`, which prints them as one 900-character dnf line inside a 134-line Containerfile. The reasoning was in better shape but worse placed: it lives in comments in containerfile.rs, where no one running kuma will ever read it. docs/desktops.md is the page that was missing. It does not try to be the inventory. `kuma generate` is the authority for that and is named as such in the first paragraph, because a hand-maintained list of seventy packages drifts within a week and buying accuracy with churn is a bad trade. What the page has instead is the part no command can emit. Which layer owns what, since an image is Fedora's minimal core, then kuma's base, then the desktop, and only the last two are kuma's curation. The line between infrastructure and applications, which existed as a rule but was written down nowhere a user could see. Why COSMIC's set is a third the size, because cosmic-session hard-requires a coherent desktop while niri requires nothing beyond itself. And the members whose presence looks arbitrary and is not: gnome-keyring-pam, nss-mdns, zram-generator-defaults, glibc-langpack-en, the Vulkan pair, the Brands font face, CJK. Every one of those was a failure someone diagnosed, and the diagnosis was recorded in a source comment and nowhere else. It also says plainly that you can add but cannot subtract, that there is no rpm_exclude, and that disliking Thunar costs a fork. That is a real limit and it belongs next to the list rather than being discovered. concepts.md carries the principle in eight lines, since it is a behaviour and belongs beside the other behaviours; the page carries the enumeration. That split also contains the staleness: only one file holds anything a package change can invalidate. The test pins the nine explained packages in both directions, still in the sets and still explained. Its first version was nearly worthless and sabotage is what showed it: searching the whole page passed even with an explanation deleted, because every package is also named in the inventory table, so the assertion held exactly when the thing it guards had been removed. It now searches only the section that explains. Both directions verified by sabotage. 134 tests, fmt and clippy clean.
1 parent 5f70f7d commit 912f5a1

4 files changed

Lines changed: 180 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ not obvious from them:
167167
than an error, what `kuma.lock` pins and what it only records, how
168168
`/etc` is merged rather than replaced, and how a bad update rolls itself
169169
back.
170+
- [What a desktop contains](docs/desktops.md): what `desktop = "niri"` or
171+
`"cosmic"` installs that you didn't name, why the surprising parts are
172+
there, and what you can change.
170173
- [For agents](docs/agents.md): the JSON surface, and why every response
171174
ends at the legal next commands.
172175
- [Contributing](CONTRIBUTING.md): smoke tests, booting a VM, iterating

‎docs/concepts.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@ Two consequences:
8181

8282
Naming a `base` opts out of all of it: any bootc image can be one.
8383

84+
## A desktop is infrastructure, your declaration is applications
85+
86+
Choosing a desktop installs packages you did not name. That set is session
87+
infrastructure: the parts that have to exist for a session to function.
88+
Applications are not in it, even convenient ones, because the two are
89+
reversible in different ways. Delete a line from your declaration and the
90+
next convergence uninstalls it; a package in a desktop set has no opt-out,
91+
so putting one there is a decision you make on behalf of everyone.
92+
93+
You can always add with `packages.rpm`. You cannot subtract from the set.
94+
[What a desktop contains](desktops.md) lists both arms, explains the
95+
non-obvious members, and says where that limit bites.
96+
8497
## kuma.lock
8598

8699
`base = "…/fedora-bootc:44"` names a tag, and tags move. One such move,

‎docs/desktops.md‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# What a desktop contains
2+
3+
Setting `desktop = "niri"` or `desktop = "cosmic"` installs a set of packages
4+
you did not name. This is what lands, and what you can do about it.
5+
6+
For the exact list any declaration produces, run `kuma generate` and read the
7+
`dnf install` line. That is the authority; this page explains it.
8+
9+
## Three layers, two owners
10+
11+
An image is built in layers, and they are not all curated by the same hand:
12+
13+
1. **Fedora's minimal bootc core.** Kernel, systemd, bootc, dnf. Fedora
14+
maintains it; kuma includes it unmodified.
15+
2. **kuma's base.** Networking, firmware, and the handful of things any real
16+
machine needs: `shadow-utils`, `sudo`, `chrony`, `openssh-server`,
17+
`passwd`, `cryptsetup`, `fwupd`. See
18+
[the base is composed, not inherited](concepts.md#the-base-is-composed-not-inherited).
19+
3. **The desktop set.** Everything below.
20+
21+
## The line between a desktop and your declaration
22+
23+
A curated desktop is session infrastructure: the parts that have to be present
24+
for a session to function at all. Applications are not in it. A calculator, an
25+
office suite, a media player, or a store belongs in `packages.flatpak` in your
26+
declaration, even when it would be convenient to bake one in.
27+
28+
The reason is reversibility rather than taste. A line in your declaration is a
29+
suggestion: delete it and the next convergence uninstalls it. A package in a
30+
desktop set is a decree, because there is no way to remove one (see
31+
[what you can change](#what-you-can-change) below). Being opinionated in an
32+
example declaration costs nothing. Being opinionated in the desktop set costs
33+
you a fork.
34+
35+
## niri
36+
37+
Hand-assembled, because niri is a window manager rather than a desktop: it
38+
requires nothing beyond itself, so every part of a working session is named
39+
explicitly.
40+
41+
| | |
42+
|---|---|
43+
| Session | `niri`, `xwayland-satellite`, `greetd`, `tuigreet` |
44+
| Shell | `waybar`, `fuzzel`, `mako`, `wob`, `swaybg`, `swayidle`, `swaylock` |
45+
| Terminal and files | `kitty`, `thunar` (+ archive plugin), `file-roller`, `gvfs`, `udiskie`, `7zip`, `unar` |
46+
| Portals | `xdg-desktop-portal-gtk`, `xdg-desktop-portal-gnome` |
47+
| Audio | `pipewire`, `pipewire-pulseaudio`, `wireplumber`, `pavucontrol` |
48+
| Graphics | `mesa-dri-drivers`, `mesa-vulkan-drivers`, `vulkan-loader` |
49+
| Hardware | `NetworkManager-wifi`, `wpa_supplicant`, `bluez`, `blueman`, `brightnessctl`, `power-profiles-daemon` |
50+
| Printing and discovery | `cups`, `system-config-printer`, `avahi`, `nss-mdns` |
51+
| Screen and clipboard | `grim`, `slurp`, `swappy`, `wf-recorder`, `wl-clipboard`, `cliphist` |
52+
| Session glue | `polkit`, `mate-polkit`, `dconf`, `gnome-keyring`, `xsettingsd`, `xdg-user-dirs`, `firewalld`, `flatpak` |
53+
| Fonts | sans, mono, emoji, CJK, and Font Awesome (free and brands) |
54+
55+
Graphical settings for wifi, bluetooth, audio, and printers are included on
56+
purpose. Those are machine state, not system definition: the declaration
57+
describes what a machine is, and picking a network is not that.
58+
59+
## COSMIC
60+
61+
Much shorter, because COSMIC curates itself. `cosmic-session` hard-requires
62+
the coherent desktop (compositor, panel, applets, settings, files, terminal,
63+
notifications, OSD, screenshot, portal, fonts), so kuma names the session plus
64+
the hardware enablement a desktop lives on, and little else.
65+
66+
The additions worth knowing: `cosmic-edit`, because the default dock pins it
67+
and the session does not require it, so without it the pin is dead;
68+
`pipewire`, because the session requires the client library but nothing pulls
69+
the daemon; and `udisks2`, because `cosmic-files` mounts removable media
70+
through it directly.
71+
72+
`cosmic-store` is deliberately absent. A store is an application, so which one
73+
a machine gets is the declaration's call.
74+
75+
## Why these are here
76+
77+
Most of a desktop set is unsurprising. These are not, and each one is a
78+
failure someone had to diagnose:
79+
80+
- **`gnome-keyring-pam`** unlocks the login keyring at login. It is a separate
81+
subpackage that nothing depends on, and the greeter's PAM lines are `-`
82+
prefixed, so a missing module is skipped in total silence. Without it, login
83+
succeeds, nothing is logged, and every keyring-using app prompts on launch
84+
forever.
85+
- **`nss-mdns`** is what makes `.local` names and driverless printer discovery
86+
actually resolve. `avahi` alone announces without resolving.
87+
- **`zram-generator-defaults`** carries the config that activates zram. The
88+
base ships the generator without it, so the desktop would have zero swap and
89+
the OOM killer would take windows under memory pressure.
90+
- **`glibc-langpack-en`** provides real locale data. The base ships
91+
`glibc-minimal-langpack`, so `en_US.UTF-8` fails to resolve and waybar's
92+
clock disables itself.
93+
- **`mesa-vulkan-drivers`** and **`vulkan-loader`**: OpenGL drivers alone
94+
strand every Vulkan application on software rendering.
95+
- **`fontawesome-6-brands-fonts`** is a second font package because the
96+
bluetooth glyph waybar uses lives in the Brands face, not Free.
97+
- **`google-noto-sans-cjk-vf-fonts`**, because the default sans is latin-only
98+
and CJK pages render as tofu.
99+
- **`avahi`** is named rather than assumed. It used to arrive with
100+
fedora-bootc by luck, and kuma's composed base does not carry it.
101+
102+
## What you can change
103+
104+
**You can add.** `packages.rpm` layers anything from Fedora's repos on top of
105+
the desktop set.
106+
107+
**You cannot subtract.** There is no `rpm_exclude` and no per-desktop opt-out,
108+
so a package in a desktop set is in your image. If you want a desktop without
109+
Thunar, the only route today is a fork.
110+
111+
That is a real limit rather than an oversight, and it is why the boundary above
112+
is drawn where it is: everything kuma cannot let you remove is something a
113+
session needs in order to work, and everything else is left to your
114+
declaration where deleting a line is enough.

‎src/containerfile.rs‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2175,6 +2175,56 @@ mod tests {
21752175
/// Native desktop ids look exactly like flatpak ids, so the handlers
21762176
/// the desktop set installs are named here rather than inferred.
21772177
/// Everything else in the list has to come from the declaration —
2178+
/// docs/desktops.md explains the desktop members whose presence is
2179+
/// not self-evident, and every one of them is a failure someone had
2180+
/// to diagnose: a silent keyring, a disabled clock, no swap, tofu.
2181+
/// Dropping one from the set while the page still explains it turns
2182+
/// the page into a lie about the image, and the reverse hides the
2183+
/// only written record of why the package is there at all.
2184+
///
2185+
/// Only these are pinned, deliberately. The full inventory's home is
2186+
/// `kuma generate`, and asking a doc to track seventy packages would
2187+
/// buy drift in exchange for churn.
2188+
#[test]
2189+
fn the_surprising_desktop_packages_are_the_ones_documented() {
2190+
const EXPLAINED: &[&str] = &[
2191+
"gnome-keyring-pam",
2192+
"nss-mdns",
2193+
"zram-generator-defaults",
2194+
"glibc-langpack-en",
2195+
"mesa-vulkan-drivers",
2196+
"vulkan-loader",
2197+
"avahi",
2198+
];
2199+
// niri-only: COSMIC's set has no waybar to lose its glyphs, and
2200+
// fonts follow the arm that renders with them.
2201+
const EXPLAINED_NIRI: &[&str] =
2202+
&["fontawesome-6-brands-fonts", "google-noto-sans-cjk-vf-fonts"];
2203+
2204+
let doc = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/desktops.md"))
2205+
.unwrap();
2206+
// Scoped to the section that does the explaining, not the whole
2207+
// page. Every package is also named in the inventory tables, so
2208+
// searching the file would assert only that the word appears
2209+
// somewhere, which stays true precisely when the explanation is
2210+
// the thing that got deleted.
2211+
let why = doc
2212+
.split_once("## Why these are here")
2213+
.and_then(|(_, rest)| rest.split_once("\n## "))
2214+
.map(|(section, _)| section)
2215+
.expect("docs/desktops.md explains the non-obvious members");
2216+
2217+
for pkg in EXPLAINED {
2218+
assert!(NIRI_PACKAGES.contains(pkg), "{pkg} left the niri set");
2219+
assert!(COSMIC_PACKAGES.contains(pkg), "{pkg} left the COSMIC set");
2220+
assert!(why.contains(pkg), "docs/desktops.md stopped explaining {pkg}");
2221+
}
2222+
for pkg in EXPLAINED_NIRI {
2223+
assert!(NIRI_PACKAGES.contains(pkg), "{pkg} left the niri set");
2224+
assert!(why.contains(pkg), "docs/desktops.md stopped explaining {pkg}");
2225+
}
2226+
}
2227+
21782228
/// and the declaration to check is the niri one, because this file
21792229
/// is only baked on the niri arm.
21802230
#[test]

0 commit comments

Comments
 (0)