Skip to content

desktops: tier system, install/remove correctness, and a pile of small fixes#824

Merged
igorpecovnik merged 42 commits intomainfrom
smallfixes
Apr 11, 2026
Merged

desktops: tier system, install/remove correctness, and a pile of small fixes#824
igorpecovnik merged 42 commits intomainfrom
smallfixes

Conversation

@igorpecovnik
Copy link
Copy Markdown
Member

Summary

A long working branch that started as "fix the desktop install on a fresh box" and grew into a complete reshape of the desktop submodule. Roughly three layers, summarised below.

Layer 1 — bug fixes that stop the existing install/remove from breaking real users

The user reported Error: YAML parser not found at /usr/bin/../tools/modules/desktops/scripts/parse_desktop_yaml.py on a fresh apt install. Tracking that down opened a series of related correctness bugs:

  • Desktop module assets weren't packaged: tools/modules/desktops/ (which holds the YAML, parser, branding assets, postinst hooks, skel, greeters) was never listed in debian.conf, so the .deb only contained lib/, bin/, share/. On installed systems the parser path resolved to /usr/tools/... which doesn't exist. Fixed: ship the directory under /usr/share/armbian-config/desktops/ and introduce a desktops_dir global in bin/armbian-config that resolves dev vs installed.
  • Skel copy regression: module_update_skel lost its recursive chown -R safety net during the refactor that added the per-file find loop. Other package postinst scripts (caja, nemo, gnome-keyring) routinely leak root-owned files into ~/.config, and without the recursive chown, MATE on first login pops up "The path for the directory containing Caja settings needs read and write permissions" — caja and nemo refuse to start because their config dir is root-owned. Restored the recursive chown.
  • Install manifest never written: the desktop refactor (Refactor desktop module: YAML-driven, modular architecture #815) kept the in-memory ACTUALLY_INSTALLED tracker in pkg_install but dropped both the persistence to disk and the file-based remove path that had been added by feat: track desktop packages for clean removal #799. The remove command was reduced to pkg_remove $DESKTOP_PRIMARY_PKG with a comment claiming "autopurge handles dependencies" — it does not, because every package in $DESKTOP_PACKAGES was manually-installed by name. Result: module_desktops remove left ~340 packages on disk. Restored /etc/armbian/desktop/<de>.packages and the file-based remove path.
  • packages_uninstall cascade traps: listing a package that is a hard Depends: of a metapackage we install causes apt's autoremove to yank the parent and a chunk of the desktop. Hit three real cases:
    • xfce4-goodies plugins (xfce4-clipman-plugin, etc.) → yanks xfce4-goodies → cascade to xserver-xorg, pulseaudio, cups, evince, mousepad.
    • language-selector-gnome → yanks gnome-control-center → user loses Settings on every gnome install.
    • kdeconnect / khelpcenter in kde-neon.yaml → would yank neon-desktop (latent — kde-neon is unsupported but the YAML is loaded).
      Dropped all three, added warning comments.
  • Install path was not fail-safe: half-installing a desktop and then flipping default.target = graphical left the next boot pinned to graphical with no DM. Now pkg_install failures return 1 cleanly without changing system state, and set-default graphical.target only fires after systemctl start display-manager returns 0.
  • module_desktops auto clobbered /etc/gdm3/custom.conf with cat > $file <<EOF, losing any user customization (WaylandEnable=false, debug, etc.). Replaced with sed-based in-place edit that preserves the rest of the file. Also fixed: it was branching on release codename (wrong on bookworm — both bookworm and trixie use daemon.conf) when it should branch on ID= from os-release.
  • module_desktops login regex: unanchored grep for AutomaticLoginEnable\\s*=\\s*true matched the commented sample line in noble's stock custom.conf, returning 0 (autologin enabled) on every fresh install. Anchored at ^.
  • `status` output was leaking into the dialog menu (it was called from every entry's condition field, dozens of times per render, and printing "minimal" / "mid" / "full" / "not installed" between the menu items). Made it silent on both paths; added a separate tier command for the value-returning getter.
  • Cinnamon armhf: installs but the panel doesn't draw because Cinnamon's compositor needs a working GL context and most armhf SBCs have no GL driver. Disabled cinnamon on armhf.
  • cp -r --update=none in module_update_skel was broken on Debian bookworm (coreutils 9.1; --update=none was added in 9.3). Replaced with a portable per-file find loop.
  • Default systemd target: install/remove now switches default.target to/from graphical.target explicitly, and the remove path additionally systemctl isolate multi-user.target so the running session drops to a console login on tty1 immediately without needing a reboot.
  • apt-get clean after remove: a full DE removal frees ~hundreds of MB of installed files but leaves the matching .debs sitting in /var/cache/apt/archives. Added a pkg_clean wrapper alongside the existing pkg_* family and call it after every desktop remove.
  • armbian-plymouth-theme is in Armbian's own apt repo, not in Debian/Ubuntu. Listing it in common.yaml's flat packages: would have caused 'apt: Unable to locate package' on every non-Armbian install. Moved it out of the YAML and gated it behind the presence of /etc/apt/sources.list.d/armbian.{list,sources}.

Layer 2 — desktop module audit cleanup

Pulled out the audit results from the in-session review:

  • Stale packages_remove cruft: every DE YAML's noble/plucky release block carried 5-6 dead entries (qalculate-gtk, hplip, indicator-printers, libfontembed1, policykit-1, printer-driver-all) copied verbatim from a long-gone explicit package list. Five of those six are no-ops (the package isn't in any install list anywhere). The sixth, printer-driver-all, was actively in xfce.yaml and mate.yaml's package lists, so the packages_remove was preventing it from being installed on noble/plucky — meaning printing was effectively non-functional on Ubuntu xfce/mate out of the box. Cleaned up the cruft and restored printer-driver-all for the DEs that ship it.
  • GNOME Quick Settings tiles: installing GNOME without the right backing daemons left the Quick Settings popover missing the Wi-Fi, Bluetooth and Power Profiles tiles. Added network-manager, bluez, gnome-bluetooth-sendto, power-profiles-daemon to gnome.yaml.
  • GNOME printing: printer-driver-all added to gnome.yaml so the Settings → Printers panel can actually add a printer.
  • module_desktops install no longer auto-installs armbian-imager — the AppImage mechanism stays available via explicit module_appimage install app=armbian-imager but isn't unconditional anymore.
  • Branding asset audit: removed 25 dead files from tools/modules/desktops/branding/ (12 unused icon-menu PNGs, 9 unreferenced wallpapers, 2 dead .desktop hider stubs, the scrcpy.svg that was misfiled here). Most importantly: removed skel/.local/share/applications/system-config-printer.desktop which was a Hidden=true stub blocking the printer-add wizard from appearing in any DE menu. Fixed three broken <filename> references in branding/armbian.xml.
  • Dialog menu: in read mode, dropped the verbose --item-help text from menu rendering so the menu output isn't 3-segment-per-entry noise.
  • GNOME language-selector-gnome warning: see Layer 1, added a warning comment in every DE YAML that touched packages_uninstall so future edits don't reintroduce the cascade.

Layer 3 — tier system

The big one. Replace the flat single-package-list-per-DE model with a tiered model where every install picks one of three tiers and can move between them later:

Tier Contents Approximate size
minimal DE + display manager + base utilities. No browser, no office, no user-facing apps beyond a terminal and a file manager. ~500 MB
mid minimal + browser + everyday user apps (text editor, calculator, image/PDF viewer, media player, archive tool, torrent client). ~1 GB
full mid + office suite + creative tools (LibreOffice, GIMP, Inkscape, Thunderbird, Audacity). ~2.5 GB

YAML schema

  • New tiers: map keyed by minimal/mid/full, replacing the flat packages: list. Each tier is additive: full = minimal + mid + full. The parser walks tiers in order.
  • New tier_overrides: for per-arch and per-release-per-arch package availability holes. Common holes (e.g. loupe missing on bookworm, thunderbird missing on Ubuntu armhf/riscv64) live in common.yaml. Per-DE overrides (e.g. KDE Plasma swapping gnome-text-editor for kate at the mid tier) live in the DE YAML.
  • New browser: virtual token. The literal string browser inside any tier resolves to a real package name from common.yaml's browser: map at parse time. Map is keyed by (release, arch) because the same arch can resolve differently across releases:
    • Debian: chromium on amd64/arm64/armhf, firefox-esr on riscv64 (Debian doesn't have a firefox package).
    • Ubuntu: epiphany-browser everywhere — Ubuntu's chromium/firefox debs are snap-shim wrappers that require snapd, which Armbian doesn't ship.
  • All 12 DE YAMLs converted to the new schema. Per-DE overrides applied to KDE Plasma (kate, drop libreoffice-gtk3) and GNOME (drop transmission-gtk).
  • Per-arch availability audit: every mid/full tier package was verified against the binary archive indices for every release × arch combo before being added to the YAML. Holes are documented in common.yaml's tier_overrides.

Bash side

  • module_desktops install now requires tier=. The parser refuses to run without --tier.
  • New manifest layout: /etc/armbian/desktop/<de>.tier (one line: minimal/mid/full) alongside the existing <de>.packages.
  • New commands:
    • upgrade de=X tier=Y — install the delta from current → target. Refuses to "upgrade" to a same/lower tier.
    • downgrade de=X tier=Y — remove the delta from current → target, intersected with the install manifest so user-installed packages are never touched.
    • set-tier de=X tier=Y — direction-agnostic, auto-detects upgrade vs downgrade. Used by the dialog menu.
    • tier de=X — value-returning getter, prints the installed tier name on stdout.
    • at-tier de=X tier=Y — silent gate, exit 0 if installed AND at the given tier. Used by dialog menu condition gates.
  • status is now silent on both paths (was leaking text into the menu).

Dialog menu

  • Each supported DE now has 3 install entries: *01 (minimal), *05 (mid), *06 (full) — flat menu, "always all three per DE" per the agreed UX.
  • For installed desktops: 3 change-tier entries *07 (to minimal), *08 (to mid), *09 (to full), each gated to be visible only when the DE is installed AND not already at that tier.
  • Existing autologin entries (*03, *04) and uninstall entry (*02) are unchanged.

What's still pending (out of scope for this PR)

  • Bandwidth confirmation dialog before mid/full installs.
  • Per-DE Plasma full tier may want to add libreoffice-style-breeze so the GTK frontend looks native; needs verification.
  • The audit identified ~10 medium-severity issues in module_desktops that aren't fixed here (sudoers in-place edit, auto clobbering gdm3 if no [daemon] section exists, container detection too narrow, etc.). Tracking issue or follow-up PR.

Testing

This PR has been tested live on a QEMU x86 noble VM through repeated install/remove/upgrade/downgrade cycles for xfce + gnome:

  • module_desktops install de=xfce tier=minimal — installs cleanly, manifest written, default.target switched
  • module_desktops install de=xfce tier=mid — same, with browser + 6 mid-tier apps
  • module_desktops install de=xfce tier=full — same, with office + creative tools
  • module_desktops upgrade de=xfce tier=mid from minimal — installs the 7-package delta
  • module_desktops upgrade de=xfce tier=full from mid — installs the 6-package delta
  • module_desktops downgrade de=xfce tier=minimal from full — removes 13 packages, intersection with manifest verified to not touch unrelated packages
  • module_desktops remove de=xfce — manifest-driven, console drops to login prompt without reboot
  • module_desktops install de=gnome tier=minimal — Quick Settings shows Wi-Fi/Bluetooth/Power Profiles tiles, Settings → Printers can add a printer, autologin enable/disable works without clobbering user customization
  • Dialog menu shows 19 install buttons (6 supported DEs × 3 tiers + KDE Neon minimal), tier-change buttons appear after install
  • All 384 (DE × release × arch × tier) parser combinations parse cleanly

Verified module_desktops auto/manual/login cycle on gnome with the in-place gdm3 edit.

How to test

git fetch origin smallfixes
git checkout smallfixes
sudo bash tools/config-assemble.sh -p

# CLI
sudo armbian-config --api module_desktops install de=xfce tier=minimal
sudo armbian-config --api module_desktops tier de=xfce             # "minimal"
sudo armbian-config --api module_desktops upgrade de=xfce tier=mid
sudo armbian-config --api module_desktops upgrade de=xfce tier=full
sudo armbian-config --api module_desktops downgrade de=xfce tier=minimal
sudo armbian-config --api module_desktops remove de=xfce

# Dialog
sudo armbian-config
# Software → Desktop shows three install buttons per DE
# After installing one, "Change to <tier>" buttons appear for the other tiers

Companion changes in other repos

  • armbian/build: PR boot: stop emitting splash=verbose to the kernel cmdline build#9653 removes the broken splash=verbose from grub.sh / grub-riscv64.sh / 24 U-Boot bootscripts, adds GRUB_GFXPAYLOAD_LINUX=text to disable Ubuntu's vt.handoff=7 injection that broke the framebuffer console after Plymouth quit on CLI installs, and fixes the agetty --noclear so first-boot gets a clean login prompt instead of stacked kernel boot messages.
  • armbian/documentation: branch developer-guide-desktops (started in a previous commit; updated in this session) — full developer guide rewrite covering the new tier model, schema, parser CLI, bash module API, lifecycle sections, common pitfalls, and security notes.

Commit list

38 commits, 49 files changed (+1835 / -1490). The commit history is intentionally not squashed: each commit is a small focused change with a descriptive message, so any individual fix can be reverted in isolation if it turns out to be wrong.

The desktop refactor (#815) replaced module_update_skel's simple
two-line copy-and-chown with a per-file find/cp/chown loop. The new
loop is internally correct in isolation, but the old recursive chown
was also serving as a safety net for root-owned files that *other*
package postinst scripts leak into the user's home on first install
(caja, nemo, gnome-keyring and others all do this).

Without that safety net, MATE on first login pops up
  "The path for the directory containing Caja settings needs
   read and write permissions"
and Nemo (with the XFCE bundle) hits the same class of bug. Both
are caused by ~/.config/{caja,nemo} being created root-owned by the
respective postinst, then never reclaimed.

Restore the previous pattern:
    cp -r --update=none /etc/skel/. "$home/"
    chown -R "$uid:$gid" "$home/"

This is a strict revert of just the install branch of
module_update_skel; nothing else from #815 is reverted.
The remove path was reduced to

    pkg_remove "$DESKTOP_DM"
    pkg_remove "$DESKTOP_PRIMARY_PKG"

with a comment claiming "autopurge handles dependencies". It does
not. pkg_install names every entry of $DESKTOP_PACKAGES on the apt
command line, so apt marks all 30+ packages (xfce4-goodies, blueman,
pavucontrol, gnome-disk-utility, terminator, numix-gtk-theme,
printer-driver-all, ...) manually-installed. apt-get autopurge of one
top-level package therefore reclaims none of its siblings, and the
vast majority of the desktop install survives the uninstall.

The original design (#799) already solved this: pkg_install does an
'apt-get -s install' dry-run, parses the "following NEW packages
will be installed" block, and appends it to ACTUALLY_INSTALLED. The
old desktop install persisted that array to
/etc/armbian/desktop/<de>.packages, and the old remove read it back
and passed it to pkg_remove. The #815 refactor kept the in-memory
tracking inside pkg_install but dropped both the persistence and
the file-based remove.

Restore the persistence:

  - Install resets ACTUALLY_INSTALLED before pkg_install, then writes
    /etc/armbian/desktop/<de>.packages from the resulting array. Skip
    the write when the array is empty so a re-install of an already-
    installed DE does not clobber the existing manifest with an empty
    file (which would make uninstall a no-op).

  - Remove reads the manifest and feeds the whole list to pkg_remove
    (apt-get autopurge), then deletes the manifest. For legacy
    installs that predate the manifest, fall back to walking
    $DESKTOP_PACKAGES through dpkg-query and removing whatever is
    currently installed. Less precise (can keep pre-existing
    packages) but it's the best we can do without the manifest.

This preserves system packages that already existed before the
desktop install (so we never yank shared deps), and removes
everything the install actually added.
After verifying which packages actually land on disk via the new
install manifest, three categories of cruft stood out:

  - packages we install only to immediately uninstall again
    (spice-vdagent — VM only; gdebi — drags ~50MB of cpp/gcc-13
    toolchain just to GUI-install .debs; gnome-system-monitor —
    duplicates xfce4-taskmanager which xfce4-goodies provides).
    These are dropped from the install list outright.

  - xfce4-goodies plugins that are either laptop-only
    (xfce4-battery-plugin), niche/dead (xfce4-mailwatch-plugin,
    xfce4-verve-plugin, xfce4-smartbookmark-plugin, xfce4-timer-plugin,
    xfce4-wavelan-plugin), need upstream API config to function
    (xfce4-weather-plugin), or redundant on SBCs where the kernel
    manages cpufreq (xfce4-cpufreq-plugin). Added to packages_uninstall.

  - Ubuntu/snap cruft pulled in transitively: apport +
    python3-apport + python3-problem-report (Canonical crash
    reporter — Armbian does not consume those reports), and
    libsnapd-glib-2-1 (snap support — Armbian does not ship snapd).
    Plus the spellcheck stack (dictionaries-common, hunspell-en-us,
    emacsen-common) which is rarely useful on a fresh SBC desktop.
    All added to packages_uninstall.

Remove ordering note: packages_uninstall runs *after* the manifest
is saved, so anything in this list still ends up in
/etc/armbian/desktop/xfce.packages even though it's gone from disk
by the time install finishes. That's harmless on uninstall (apt
just skips the not-installed names) but the manifest is misleading;
fixing that ordering is a separate concern.
Pulls the Armbian-branded boot splash in for every DE install via
common.yaml so we don't have to repeat it in each per-DE file.
The previous commit added the xfce4-goodies plugins (battery,
mailwatch, weather, ...) plus a few unrelated entries to
packages_uninstall, expecting apt to drop just those. On any system
with apt's autoremove behavior enabled (which is the Ubuntu noble
default), removing one xfce4-goodies dependency yanks the
xfce4-goodies meta package itself, and the resulting cascade rips
out half the desktop:

  cups* cups-core-drivers* cups-daemon* dictionaries-common*
  emacsen-common* evince* hunspell-en-us* libenchant-2-2*
  libevview3-3t64* libgspell-1-2* libsnapd-glib-2-1* mousepad*
  pulseaudio* pulseaudio-module-bluetooth* python3-apport*
  python3-problem-report* ristretto* xfburn* xfce4-battery-plugin*
  xfce4-clipman* xfce4-clipman-plugin* xfce4-cpufreq-plugin*
  xfce4-dict* xfce4-goodies* xfce4-mailwatch-plugin*
  xfce4-smartbookmark-plugin* xfce4-terminal* xfce4-timer-plugin*
  xfce4-verve-plugin* xfce4-wavelan-plugin* xfce4-weather-plugin*
  xserver-xorg*

Drop every entry that is a Depends of xfce4-goodies. The
spellcheck stack (dictionaries-common, hunspell-en-us,
emacsen-common) and gnome-system-monitor are also gone since
they were transitively pulled by the same goodies graph.

Keep only the entries that are orthogonal to the xfce dep tree:
apport + python3-apport + python3-problem-report (Ubuntu crash
reporter — Armbian doesn't consume those reports) and
libsnapd-glib-2-1 (snap support, no snapd in Armbian).

Add a comment at the top of packages_uninstall warning future
edits not to put xfce4-goodies depends here.
The Armbian Imager AppImage is Qt6/RHI based and dlopen()s
libGLESv2.so.2 at startup. On a system where no desktop has yet
pulled in the GL stack — or after an autopurge yanks it as an
orphan — running the imager fails with

  error while loading shared libraries: libGLESv2.so.2:
  cannot open shared object file

Make module_appimage install the GL/EGL/GLES runtime explicitly so
the imager can launch even when the GL stack is not pulled
transitively by some desktop dep tree. The packages are tiny,
shared with every DE, and idempotent if already installed.
The noble and plucky packages_remove blocks contained six entries
copied from a long-dead explicit package list:

  qalculate-gtk hplip indicator-printers libfontembed1 policykit-1
  printer-driver-all

Five of those six are no-ops: none of them is in xfce.yaml's
packages: list, in common.yaml's packages: list, or in any
release-specific packages: block, so packages_remove (which only
filters the install list) has nothing to filter and they were just
confusing future readers. The bookworm packages_remove had a
similar stale stub (libfontembed1, update-manager{,-core}).

The sixth, printer-driver-all, IS in xfce.yaml packages: line 34,
so packages_remove was actively preventing it from being installed
on noble and plucky. The result is that on Ubuntu xfce installs
the user gets cups, cups-daemon, system-config-printer — but no
actual printer drivers — so the printer-add wizard cannot
configure anything. Printing was effectively non-functional out
of the box on noble/plucky xfce.

Drop the dead entries from bookworm/noble/plucky packages_remove
and drop printer-driver-all from noble/plucky specifically. Keep
the trixie pulseaudio->pipewire entries (which are the only
legitimate packages_remove in this file) and keep plucky's
pavumeter entry (the package was dropped from the plucky archive).
The 'read' fallback path for dialog_menu was rendering --item-help
triplets as

  1. CINM01 - Install Cinnamon - Install the Cinnamon desktop environment
  2. GNME01 - Install GNOME - Install the GNOME desktop environment
  ...

The third segment is the dialog --item-help text, which is meant
for the F1/hover popup in real dialog/whiptail. In read mode there
is no popup so we were just printing it inline, which doubles up
with the description and produces a noisy three-segment line on
every menu. Drop the third segment in this branch and just print
'<tag> - <description>'. Real dialog/whiptail paths are unaffected.
skel/.local/share/applications/system-config-printer.desktop is a
two-line hider stub:

  [Desktop Entry]
  Hidden=true

When module_update_skel install copies the skel tree into every
user's ~/.local/share/applications/, the XDG menu spec applies the
Hidden=true override on top of the system-installed
/usr/share/applications/system-config-printer.desktop, hiding the
printer-add wizard from every desktop menu.

That is exactly the symptom reported on a fresh xfce install:
"why don't I have a Printers icon in the menu?". The package was
installed (system-config-printer is in xfce.yaml), the binary
was on PATH, the system .desktop file was valid — but the per-user
hider stub was actively suppressing the menu entry.

The stub appears to be collateral from a long-ago "hide every
xfce-vendor entry" sweep. With printer-driver-all back in the
install list and printing now expected to work out of the box,
this stub has to go.
The branding/icons/ directory was carrying twelve icon-menu-*.png
swatches, an icon-armbian-config-penguin.png variant, and
scrcpy.svg — none of which are referenced anywhere in the
codebase. The branding install in module_desktop_branding.sh
copies the entire directory to /usr/share/icons/armbian/ but only
armbian-config.png is actually consumed (by
share/applications/configng.desktop). The rest are dead weight
shipped in the .deb for no reason.

scrcpy.svg in particular was clearly misfiled — it's an icon for
the Android screen-mirroring tool, completely unrelated to desktop
branding.

Also drop skel/.local/share/applications/gdebi.desktop. It was a
hider stub for the gdebi GTK frontend, which we already removed
from xfce.yaml's package list — there's nothing left to hide.

Single icon kept: armbian-config.png. Verified via grep across
the entire repo that nothing else references the deleted files.
armbian.xml is the GNOME wallpaper picker manifest installed at
/usr/share/gnome-background-properties/armbian.xml. It points
gnome-control-center / nautilus at the wallpapers in
/usr/share/backgrounds/armbian/. Three of its <filename> entries
referenced files that did not exist on disk:

  - armbian-4k-green-retro.jpg  (real file: armbian-4k-retro-green.jpg)
  - armbian-4k-purple-penguine.jpg  (real file: armbian-4k-purple-penguin.jpg)
  - armbian-full-under-construction-3840-2160.jpg
        (real file: armbian-full-undeer-construction-3840-2160.jpg)

The first two looked like word-order/typo bugs in the XML; the
third is a typo in the filename on disk ("undeer") that's old
enough that fixing the disk side would just rename a file with no
upstream reference. Fix the XML to match the disk in all three
cases. Also normalize the indentation on the purple-penguin block,
which had stray leading spaces.

Verified: every <filename> entry now resolves to an existing file
in branding/wallpapers/.
Nine wallpaper files in branding/wallpapers/ are not referenced
from armbian.xml (the GNOME wallpaper picker manifest), any
postinst dconf override, the slick-greeter config, the GNOME
postinst dconf settings, or any skel file. They were just being
copied to /usr/share/backgrounds/armbian/ with no path that
exposes them to the user:

  Black-Red--Fractal-Abstract-Armbian-Centered_3840x2160.jpg
  Black-Red--Fractal-Abstract-Armbian-Right_3840x2160.jpg
  Black-Red-Abstract-Wave-Circle-Armbian-Centered_3840x2160.jpg
  Black-and-Red-Striped-Arrow-Abstract-Armbian-Centered_3840x2160.jpg
  Black-and-Red-Striped-Arrow-Abstract_Armbian_Right_3840x2160.jpg
  armbian-1080p-evolution.jpg
  armbian-1080p-love.jpg
  armbian-4k-penguin-SBC.jpg
  armbian-4k-penguin-minimalistic.jpg

The Black-Red set is from a different visual style than the rest
of the 4k library and was never wired into armbian.xml. The two
1080p ones are below the resolution we now ship at. The two
"penguin-*" ones don't have armbian.xml entries either.

Wallpapers under wallpapers-lightdm/ are intentionally left
alone: even though only one of them is the slick-greeter default,
they exist as a paired blurred set for the wallpapers we still
ship and are valid swap targets for /etc/lightdm/slick-greeter.conf.
A full desktop uninstall purges several hundred MB of installed
files but leaves the matching .deb archives sitting in
/var/cache/apt/archives. On SBC root filesystems that's wasted
space the user almost never reclaims manually.

Add a pkg_clean wrapper alongside the existing pkg_install /
pkg_remove / pkg_update / etc. functions so desktops (and any
other module that needs it later) don't have to bypass the
abstraction with raw apt-get calls. Wire apt_operation_progress
to recognise the 'clean' operation for its dialog title.

Call pkg_clean at the end of the desktop remove path. Failure is
non-fatal because pkg_clean inherits apt_operation_progress's
return code handling.
Drop the implicit 'module_appimage install app=armbian-imager'
call from the desktop install flow, and the matching remove call
from the uninstall flow. Users who want the imager can still get
it explicitly via module_appimage install — the mechanism stays
intact, only the unconditional default goes away.

Side effects of dropping it:
  - libfuse2 / fuse3 are no longer installed unconditionally on
    every desktop install. They were only needed for AppImage
    runtime, and module_appimage installs them itself when the
    user actually runs an appimage install.
  - libgles2 / libegl1 / libgl1 / libgl1-mesa-dri are also no
    longer pulled in via this path, but a regular desktop pulls
    them in transitively via xserver-xorg, so this is a no-op
    for normal desktop installs.

The remove side cleanup is symmetric: no point trying to remove
an AppImage we didn't install.
armbian-plymouth-theme lives in Armbian's own apt repo and is not
available from Debian/Ubuntu mirrors. Listing it in common.yaml's
packages: caused 'apt-get install' to hard-fail with 'Unable to
locate package' on non-Armbian systems, aborting the entire
desktop install.

Move it out of common.yaml so the YAML stays distro-agnostic.
Install it explicitly from the desktop install path, gated on the
presence of the Armbian apt source — accept either the legacy
/etc/apt/sources.list.d/armbian.list or the modern deb822
/etc/apt/sources.list.d/armbian.sources. Failure of that one
pkg_install call is non-fatal: it logs a warning and continues.
…i3-wm/kde-plasma

Same cleanup as the earlier xfce.yaml commit, applied to the
remaining DE files. Every release block carried the same dead
five-entry list:

  qalculate-gtk hplip indicator-printers libfontembed1
  policykit-1 (+ printer-driver-all on noble/plucky)

These were copied verbatim from a long-dead explicit package list
during the YAML refactor. Of those:

  - qalculate-gtk, hplip, indicator-printers, policykit-1,
    libfontembed1, update-manager, update-manager-core: not in
    any DE's packages: list, so packages_remove (which only
    filters the install list) had nothing to filter. Pure noise.

  - printer-driver-all: actively in cinnamon.yaml and mate.yaml
    packages: lines, so packages_remove was preventing it from
    being installed on noble/plucky for both — same printing
    breakage we already fixed for xfce. gnome.yaml does not
    install printer-driver-all in the first place, so dropping
    it from gnome's packages_remove is a no-op for printing
    (gnome users will not get printer drivers from this YAML
    sweep alone — that's a separate decision).

Drop the dead block from every noble/plucky packages_remove and
the bookworm libfontembed1/update-manager stub. Keep the trixie
pulseaudio->pipewire packages_remove (legitimate). Keep plucky's
pavumeter entry (the package was dropped from the plucky archive)
and add a comment so the next reader knows why.

Verified via parse_desktop_yaml.py: cinnamon and mate now ship
printer-driver-all on noble; all five YAMLs parse cleanly; the
five DEs' DESKTOP_PACKAGES_UNINSTALL output is unchanged for the
entries that legitimately belonged there.
The Quick Settings popover (top-right of the GNOME panel) was
missing the Wi-Fi, Bluetooth and Power Profiles tiles on a fresh
install. Each tile is rendered by gnome-shell only when the
corresponding daemon/library is present, and on a minimal Ubuntu
noble install with --no-install-recommends none of them are
pulled in transitively:

  - Wi-Fi / Wired tile     -> needs the NetworkManager daemon
                              (network-manager). The list already
                              had network-manager-gnome but that
                              is just the legacy nm-applet GUI
                              and is unrelated to the tile.

  - Bluetooth tile         -> needs bluez (the daemon) plus
                              gnome-bluetooth-sendto, which
                              pulls in libgnome-bluetooth-3.0
                              that gnome-shell dlopens to render
                              the tile. (gnome-bluetooth on noble
                              is now a transitional shim pointing
                              at gnome-bluetooth-sendto.)

  - Power Profiles tile    -> needs power-profiles-daemon, which
                              gnome-control-center only Recommends.

Add bluez, gnome-bluetooth-sendto, network-manager and
power-profiles-daemon to gnome.yaml's packages: list. Verified
each package against packages.ubuntu.com/noble before adding.
cups was already in common.yaml so the spooler is present, but
gnome.yaml shipped no actual printer drivers. The Printers panel
inside GNOME Settings (which is built into gnome-control-center
on noble — no separate system-config-printer needed) had nothing
to expose, so users could not add a real printer.

Add printer-driver-all to gnome.yaml's main packages list. Pulls
HPLIP, Gutenprint, Foomatic, Splix and friends. Verified the
parser still produces a valid install list.
…rol-center)

The noble/plucky packages_uninstall blocks in gnome.yaml,
cinnamon.yaml, mate.yaml and xfce.yaml all listed
language-selector-gnome alongside ubuntu-session. The intent
was to suppress Ubuntu's "Language support is not installed"
first-login nag.

The actual effect is much worse: gnome-control-center has a
hard dependency on language-selector-gnome on noble, so when
the install path runs

  apt-get remove -y --purge language-selector-gnome

apt cascades and yanks gnome-control-center along with it. The
manifest record sees gnome-control-center as installed (it was,
moments ago) but by the time the user logs in, Settings is gone.
Same class of bug as the xfce4-goodies cascade we already fixed.

Drop language-selector-gnome from packages_uninstall in all four
DE files. Keep ubuntu-session — that one we genuinely do want
gone (it strips Ubuntu's branded session entry from gdm and
nothing depends on it). Add a warning comment so the next person
who looks at the list does not re-add it.

Verified the YAML still parses cleanly for all four DEs.
gnome-control-center on Ubuntu noble ships
/usr/share/applications/gnome-ubuntu-panel.desktop, which points
at the icon name preferences-ubuntu-panel. That icon only exists
in Ubuntu's icon theme. On a non-Ubuntu-themed install like
Armbian, the entry renders in the GNOME app grid as a broken
grey-triangle icon labelled "Proxy" (the localized Name=).

We can't delete the .desktop file directly because dpkg owns it
and any gnome-control-center upgrade would put it back. Removing
the package itself is also out — gnome-control-center is the same
package that gives us Settings and the rest of the panels.

Drop a NoDisplay=true / Hidden=true stub at
/usr/local/share/applications/gnome-ubuntu-panel.desktop instead.
The XDG spec gives /usr/local/share/applications precedence over
/usr/share/applications, so the hider survives package upgrades
and applies to every user without skel copy magic.
/etc/os-release on Armbian sets LOGO="armbian-logo", and GNOME
Settings -> About reads that key and calls
gtk_icon_theme_lookup_icon("armbian-logo", ...) to render the
distributor logo. Without an installed icon by that name, GNOME
falls back to ID=ubuntu and renders the Ubuntu mark instead.

We already ship branding/pixmaps/armbian.png in the repo, but
module_desktop_branding only copies it to
/usr/share/pixmaps/armbian/armbian.png — a path the freedesktop
icon spec does not search.

Install the same image to
/usr/share/icons/hicolor/256x256/apps/armbian-logo.png so the
hicolor theme lookup resolves it. Refresh the hicolor cache via
gtk-update-icon-cache so the change is visible without re-login.
Both steps fail open: missing source file or missing
gtk-update-icon-cache binary just skips the step.
The previous commit installed branding/pixmaps/armbian.png to
/usr/share/icons/hicolor/256x256/apps/armbian-logo.png, but the
PNG we ship is actually 128x128. The hicolor icon cache
validates that an icon file's actual pixel dimensions match the
parent directory name, so a 128x128 image under 256x256/apps/
gets silently excluded from the cache lookup. Result: the file
was on disk but gnome-control-center still fell back to the
Ubuntu logo.

Install to 128x128/apps/ instead, where the cache will accept
it. Also remove any stale 256x256 install left over from the
previous (broken) version of this step so we don't leave
orphaned files behind on systems that already ran the bad
postinst.
The previous attempt to hide Canonical's broken-iconed "Ubuntu"
panel entry by dropping a NoDisplay=true / Hidden=true stub at
/usr/local/share/applications/gnome-ubuntu-panel.desktop
broke gnome-control-center entirely.

The reason: gnome-ubuntu-panel.desktop is NOT a normal application
launcher. It's a gnome-control-center panel descriptor — the
compiled-in panel walk loads it via cc_shell_model_set_panel_visibility
and trips an assert(valid) when the desktop file is missing
required keys (which our stub deliberately omitted). Trace from
the affected box:

  WARNING: Ignoring broken panel ubuntu (missing desktop file)
  ERROR: ../shell/cc-shell-model.c:412:cc_shell_model_set_panel_visibility:
         assertion failed: (valid)
  Bail out!
  Aborted

After this, gnome-control-center cannot launch at all — Settings
just hangs/never opens. Same chain of events on every fresh
install of gnome on the smallfixes branch since the previous
commit.

Strip the hider stub and replace it with an unconditional rm -f
so any system that already ran the broken postinst is recovered
on the next install. The "Proxy" broken-icon entry returns, but
that is a cosmetic annoyance compared to a fully unusable
Settings app.

A real fix for the broken icon needs to either ship a
preferences-ubuntu-panel icon in the icon theme, or patch
gnome-ubuntu-panel.desktop's Icon= line in place to point at a
name we already have. Both require editing the dpkg-owned file
and are out of scope for this revert.
The 128x128/apps install from the previous commit doesn't work
on noble: even though the directory IS listed in
/usr/share/icons/hicolor/index.theme as a valid 128x128
Threshold/Applications directory, gtk-update-icon-cache rebuilds
without errors but the resulting cache contains zero entries for
armbian-logo. Verified on the affected box:

    strings /usr/share/icons/hicolor/icon-theme.cache | grep armbian
    -> 0 matches

I don't have a tidy explanation for the silent miss but I do
have a working alternative: install at scalable/apps. The
scalable directory has Type=Scalable semantics in the index and
accepts any PNG without dimension validation; the cache picks
it up reliably on noble.

Drop our 128x128 PNG into scalable/apps/armbian-logo.png and
clean up the stale 128x128 (and 256x256, just in case) installs
from previous attempts. Update the surrounding comment to record
the cc-about-page.c lookup chain so the next person who reads
this knows exactly which icon names gnome-control-center is
hunting for and why.
…base-files

The previous three commits tried to install armbian-logo.png from
module_desktop_branding so that gnome-control-center's Settings
-> About page would render the Armbian penguin instead of the
Ubuntu mark. None of them produced a visible logo on the test
box despite GTK4's icon-theme lookup confirming that the icon
was findable:

  python3 ... has=True for 'armbian-logo'

This is the wrong layer. /etc/os-release sets LOGO="armbian-logo"
from armbian-base-files; the matching icon should ship from the
SAME package so that:

  - the LOGO= value and the icon file are versioned together,
  - the dpkg trigger from hicolor-icon-theme rebuilds the cache
    automatically (no gtk-update-icon-cache shell-out from us),
  - every Armbian image gets the right logo whether or not the
    user installs a desktop, and every consumer that reads
    LOGO= from os-release (gnome-control-center, KDE Info
    Center, neofetch, fastfetch, inxi, ...) gets it for free,
  - armbian-config stops being responsible for distro branding,
    which it never should have been in the first place.

Drop the icon-install block from module_desktop_branding.sh.
Keep an unconditional rm -f for the three paths previous commits
might have written to, so any system that ran a broken version
of this step gets a clean slate when armbian-base-files later
takes over.

The actual icon ship belongs in armbian-base-files and will be
done as a separate change in that repo.
After uninstalling a desktop, users were left staring at a blank
tty1 with no login prompt — the display manager was purged but
the system still tried to reach graphical.target on the next
boot. With no DM behind it, graphical.target arrived empty and
getty@tty1 (which has Conflicts=display-manager.service) never
started.

Make the install/remove pair fix the systemd default target
explicitly:

  install -> systemctl set-default graphical.target
  remove  -> systemctl set-default multi-user.target

The install side is mostly defensive — every DM postinst already
does this for itself — but doing it from here means a partial
install or a re-install always lands on graphical.target without
needing to trust the DM's postinst to have run successfully.

The remove side is the actual fix. After 'systemctl set-default
multi-user.target' the next boot brings up the regular console
login regardless of which DM was previously installed.

For the running session (so the user does not have to reboot to
get a login prompt) the remove side also runs

  systemctl isolate multi-user.target

immediately after stopping display-manager. Just starting
getty@tty1.service on its own does not work while
graphical.target is still active — it stays in 'inactive (dead)'
because of the Conflicts= relationship — so isolate is the only
reliable way to force the running session to drop to the
console. Isolate is destructive (kills any open GUI sessions),
but the GUI is being torn down anyway.

All systemctl calls are wrapped in '2>/dev/null || true' so
failure is never fatal — for example inside a container where
systemd is not init.
The previous version used 'cp -r --update=none /etc/skel/. $home/'
to copy skel content into existing user homes without
overwriting user-edited files. The '--update=none' long-form
flag was added in GNU coreutils 9.3 — Debian bookworm ships
9.1 and rejects the option. On bookworm, the cp call exits
with 'unrecognized option' and copies nothing, silently
losing every skel file. (The chown -R safety net still runs
and saves us from leaving root-owned files behind, but the
skel content itself is lost.)

The obvious alternative '-n' / '--no-clobber' is no good
either: it works on bookworm but on coreutils 9.2+ (so on
noble's 9.4) '-n' prints a diagnostic to stderr and exits
nonzero whenever it skips a file, which on a normal repeat
invocation is every file. Neither flag is portable across
bookworm and noble.

Use a per-file find loop instead. Walk /etc/skel and copy
each entry only if it doesn't already exist at the
destination. find walks parents before children, so
intermediate directories are created before any of their
contents arrive. After the per-file copy, run chown -R on
the entire home as a safety net for root-owned files that
other package postinst scripts (caja, nemo, gnome-keyring,
etc.) routinely leak into ~/.config — see commit 462281b
for the original recursive-chown rationale.

Verified across the five scenarios:
  1. fresh skel into empty $HOME    -> all files copied
  2. existing skel file, user-edited -> user's version preserved
  3. extra files in $HOME             -> untouched
  4. nested directories               -> created and populated
  5. repeat run                        -> idempotent, no-op
The kde-neon.yaml packages_uninstall list contained four
entries that would trigger the same apt cascade bug we
already fixed for xfce4-goodies and language-selector-gnome:

  - kdeconnect    (Depends of neon-desktop)
  - khelpcenter   (Depends of neon-desktop)
  - gnome-keyring (pulled by KDE/GTK pam dependencies)
  - libreoffice*  (bash glob, not apt — apt does not expand
                   globs and bash glob-expands in cwd first)

If a user actually tries to install kde-neon (it is currently
status: unsupported but the YAML still parses), the install
path runs

  apt-get remove -y --purge kdeconnect khelpcenter ...

and apt's autoremove cascades, yanking neon-desktop and a
large chunk of the plasma stack along with it.

Drop those four entries. Keep gnome-software and thunderbird
which are orthogonal to the plasma dep tree and safe to
remove. Add a comment block documenting the cascade rule so
the next person editing this file does not re-introduce them.

The libreoffice glob deserves a separate fix (we should
enumerate the actual binary package names if we want
libreoffice gone) but that is out of scope for this commit.
Two related fixes to module_desktops.sh, both in the install /
auto-login path.

1) install: bail on pkg_install failure, only flip default.target
   AFTER the DM has actually started.

   Previously, if 'pkg_install $DESKTOP_PACKAGES' or
   'pkg_install $DESKTOP_DM' failed midway (broken mirror,
   half-resolved deps, disk full), the install path kept going:
   branding installed, groups added, default.target switched to
   graphical, 'systemctl start display-manager' attempted but
   ignored if it failed. The next boot would land on
   graphical.target with no working DM and the user would see a
   black screen.

   Check the return of each pkg_install. If either fails, return
   1 from the install branch with no further state changes — the
   manifest is not written, default.target stays where it was,
   no DM is started. The system is in the same state as if the
   install had never run.

   Also delay the 'systemctl set-default graphical.target' call
   until the start-display-manager step actually returned 0. If
   the DM unit refuses to start, the next boot would otherwise
   pin to graphical with a broken DM — the symptom we're trying
   to avoid.

2) auto: edit gdm3 conf in place instead of clobbering it.

   The previous code did 'cat > /etc/gdm3/custom.conf <<EOF
   [daemon] AutomaticLoginEnable = true AutomaticLogin = $user
   EOF' which overwrites the entire file, losing any user
   customization (WaylandEnable=false, debug=true, accessibility
   keys, etc.). The lightdm and sddm paths are safe — they write
   to drop-in directories — but the gdm3 path was destructive.

   gdm3 has no conf.d drop-in support upstream or in
   Debian/Ubuntu patches (verified against gdm3 43.0-3 / 46.2 /
   48.0-2 patch series). It loads exactly one config file. So
   we have to edit it in place. Use a small sed-based block
   that:
     - picks /etc/gdm3/daemon.conf on Debian and
       /etc/gdm3/custom.conf on Ubuntu, branching on
       'ID=' from /etc/os-release rather than on release
       codename. The previous codename check
       ([[ trixie || forky ]]) was wrong because Debian
       bookworm also reads daemon.conf, not custom.conf.
     - creates the file from scratch when it doesn't exist
     - inserts a [daemon] section if the existing file is
       missing one
     - updates AutomaticLoginEnable / AutomaticLogin in place
       when present, inserts them after [daemon] when not
   The 'manual' branch is also tightened to handle whitespace
   variation around '=' via sed -E.

   Verified against five test cases: fresh install, partial
   existing config, idempotent re-enable, manual disable,
   no-[daemon]-section file. All five preserve user-edited keys
   like WaylandEnable=false in adjacent positions.
Adds a tier model to the desktop YAML so each DE can be installed
at one of three sizes:

  minimal — DE + display manager + base utilities. ~500 MB.
  mid     — minimal + browser + everyday user apps (text editor,
            calculator, image/PDF viewer, media player, archive
            tool, torrent client). ~1 GB.
  full    — mid + office suite + creative tools (LibreOffice,
            GIMP, Inkscape, Thunderbird, Audacity). ~2.5 GB.

Tiers are additive: full = minimal + mid + full. The parser walks
them in order and accumulates packages.

Schema changes
--------------
Replace each per-DE YAML's flat 'packages:' / 'packages_uninstall:'
with a 'tiers:' map keyed by tier name. Each tier carries its own
'packages:' and (for minimal) 'packages_uninstall:'. Per-DE YAMLs
can also add 'packages_remove:' under a tier to drop entries
inherited from common.yaml — see kde-plasma.yaml for an example
that swaps gnome-text-editor for kate at mid and libreoffice-gtk3
for libreoffice-kde at full.

Add 'tier_overrides:' as a per-DE escape hatch for per-arch
removals. Use it to drop packages that don't exist on a particular
arch (e.g. blender/inkscape on armhf, libreoffice on riscv64).
The xfce.yaml example below shows the shape; populating it for
the rest is a follow-up after the per-arch availability audit.

The orthogonal 'releases:' block keeps its existing semantics:
per-release architectures, packages_remove, packages, and
packages_uninstall apply to whatever tier is being installed.

common.yaml carries the per-tier defaults that apply to every
desktop. Per-DE YAMLs only need a tiers block when they want to
add to or override common — most of the supported DEs (xfce,
gnome, mate, cinnamon, i3-wm, xmonad, enlightenment) only declare
their own minimal tier and inherit common's mid/full unchanged.

Browser substitution
--------------------
The literal token 'browser' inside any tier resolves to a real
package name from common.yaml's 'browser:' map at parse time.
Default mapping: chromium on amd64/arm64/armhf, firefox on
riscv64. No google-chrome, no Microsoft Edge — only in-archive
native packages.

Parser changes
--------------
parse_desktop_yaml.py grows a mandatory --tier flag:

  parse_desktop_yaml.py <yaml_dir> <de> <release> <arch> --tier <tier>

The flag has no default. Calling the parser without --tier prints
the usage and exits 1. The --list, --list-json, and --primaries
modes are unchanged (they don't need a tier — they only enumerate
DEs and report their primary package).

A new bash output variable DESKTOP_TIER is emitted alongside the
existing DESKTOP_PACKAGES / DESKTOP_PACKAGES_UNINSTALL / etc.
The bash side will use it in a follow-up commit to write the
/etc/armbian/desktop/<de>.tier marker file.

Coverage
--------
All 12 DE YAMLs converted to the new schema:
  Supported (8): xfce, gnome, mate, cinnamon, kde-plasma, i3-wm,
                 xmonad, enlightenment.
  Unsupported (4): budgie, deepin, kde-neon, bianbu — converted
                   so the parser doesn't crash, but no tier
                   overrides since they're not actively maintained.

Verified the parser produces the expected output for every
supported DE × {minimal,mid,full} on noble/amd64, and that the
KDE per-DE overrides (kate replacing gnome-text-editor,
libreoffice-kde replacing libreoffice-gtk3) actually apply.

Note: this commit only changes the schema and the parser. The
bash install path still ignores tiers — the next commit wires
'install tier=<tier>' through to the parser.
Companion to the previous commit (parser + YAML schema). This
commit makes the bash side actually use the new tier system.

module_desktop_yamlparse
------------------------
Add an optional 4th positional arg `tier`, defaulting to 'minimal'
when omitted. The default keeps `auto`/`manual`/`login`/`status`
calls (which only need DESKTOP_DM and DESKTOP_PRIMARY_PKG, both of
which are already correct at the minimal tier) working without
modification. The new bash variable DESKTOP_TIER is exported
alongside the existing ones.

module_desktops install
-----------------------
- `tier=minimal|mid|full` is now mandatory. Reject early with a
  clear message instead of letting the parser bail with a generic
  usage error.
- Pass tier through to the parser via the new 4th yamlparse arg.
- Always write /etc/armbian/desktop/<de>.tier alongside the
  existing <de>.packages manifest. Written even when the install
  added no new packages (re-install at the same tier) so the
  marker stays accurate.

module_desktops remove
----------------------
- Read /etc/armbian/desktop/<de>.tier and pass the installed tier
  to the parser, so the YAML fallback (when the manifest is
  missing) walks the right tier's package list. Defaults to
  'minimal' if no marker exists.
- Clean up the .tier marker file alongside .packages.

module_desktops status
----------------------
- When the desktop is installed, print the installed tier name
  (minimal/mid/full) as stdout, returning 0. When not installed,
  print "not installed" and return 1. Old callers that only
  cared about the exit code keep working.

module_desktops upgrade / downgrade  (NEW)
------------------------------------------
Two new commands that move an installed desktop between tiers:

  module_desktops upgrade   de=xfce tier=mid
  module_desktops upgrade   de=xfce tier=full
  module_desktops downgrade de=xfce tier=minimal
  module_desktops downgrade de=xfce tier=mid

Implementation in the new private helper
_module_desktops_change_tier:

  1. Read /etc/armbian/desktop/<de>.tier (must exist).
  2. Validate the direction: upgrade refuses to move to a same
     or lower tier, downgrade refuses to move to a same or
     higher tier. Both refuse if the target equals the current
     tier. Use the corresponding command instead.
  3. Parse the YAML twice — once at current tier, once at target.
  4. Compute the symmetric set difference via awk on per-line
     printf input. Read the package strings into bash arrays
     first; quoting matters because the previous attempt with
     unquoted vars put every package on a single line and broke
     the awk comparison.
  5. Upgrade: pkg_install the (target - current) delta. Append
     the newly-installed packages to the manifest.
  6. Downgrade: pkg_remove the (current - target) delta,
     INTERSECTED with the install manifest. The intersection is
     critical: it ensures we only ever remove packages we
     ourselves installed. Packages the user installed manually
     after the desktop install (and which happen to also be
     named in the YAML) are never touched.
  7. Update the .tier marker.

Failure modes are handled:
  - missing marker file -> clear error, nothing changed
  - same tier (no-op) -> print message, exit 0
  - wrong direction -> error pointing at the right command
  - pkg_install / pkg_remove failure -> error, marker NOT updated

Verified end-to-end: parser produces correct deltas for
xfce {minimal,mid,full} on noble/amd64 (7 + 6 packages added in
sequence), and KDE Plasma overrides correctly produce a delta
without gnome-text-editor / file-roller / loupe / libreoffice-gtk3
and WITH kate / libreoffice-kde at the equivalent positions.

Note: this commit does NOT add the dialog menu entries for tier
selection. The bash --api still works directly. The dialog
plumbing is the next commit.
The previous commit made tier= mandatory on 'module_desktops
install'. Without this companion change, every existing dialog
menu install entry (CINM01, GNME01, MATE01, I3WM01, KDEP01,
KDEN01, XFCE01) would fail with "Error: specify tier=" when
clicked.

Add tier=minimal to each install command so the existing menu
keeps working with its current "one button per DE" UX. This
preserves behavior bit-for-bit: clicking "Install XFCE" still
installs the same set of packages it always did.

Adding mid/full menu entries (one button per DE per tier as
agreed) and the change-tier UX for already-installed desktops
will land in a follow-up commit. Doing it here would balloon
this PR; the underlying bash and parser changes need to be
testable in isolation first.
Per the agreed UX ("one step menu, always all three per DE"),
add 14 new install entries to the desktop menu:

  CINM05 / CINM06   Cinnamon mid / full
  GNME05 / GNME06   GNOME mid / full
  MATE05 / MATE06   MATE mid / full
  I3WM05 / I3WM06   i3 mid / full
  KDEP05 / KDEP06   KDE Plasma mid / full
  XFCE05 / XFCE06   XFCE mid / full

Each tier entry has its own help text describing what's added
and the rough install size (~500 MB / ~1 GB / ~2.5 GB). The
help string is what dialog shows on F1 / hover.

ID slot allocation
------------------
Existing convention is 4-char DE prefix + 2-digit action code.
The desktop block already used:
  *01 install (minimal)
  *02 uninstall
  *03 enable autologin
  *04 disable autologin

So the new tier entries have to start at *05. Picked:
  *05 = mid install
  *06 = full install

This keeps the 6-char ID convention and avoids collisions with
the existing autologin blocks. Verified across the whole
config.system.json file: zero duplicate IDs after the change.

Each new entry inherits the same condition as the corresponding
*01 entry — `! module_desktops installed && module_desktop_supported X`
— so all three tier choices for a given DE appear simultaneously
when no desktop is installed, and disappear together once any
desktop is installed.

Skipped
-------
- KDE Neon stays at the single KDEN01 minimal entry. It's
  status: unsupported and there's no point expanding the menu
  for it.
- xmonad and enlightenment are not in the menu today (despite
  being status: supported). Adding them is a separate UX
  question and out of scope here.
- Change-tier UX (upgrade/downgrade from inside the dialog
  for an already-installed desktop) is also a separate concern
  — needs a per-DE-per-current-tier conditional matrix and
  belongs in its own commit.
After auditing every mid/full tier package against the actual
Debian / Ubuntu archives, several holes need handling so the
install path never tries to apt-install a non-existent package.
This commit handles all the holes plus a couple of related
correctness bugs.

1) libreoffice-kde does not exist in any release (retired
   upstream). kde-plasma.yaml's full tier was swapping in
   libreoffice-kde, which would have failed on every install.
   Drop the swap; plain libreoffice from common.yaml's full
   tier inherits KDE integration via libreoffice-style-breeze
   when Plasma is installed.

2) Browser map is now keyed by (release, arch), not just arch.
   Required because:
     - Debian has 'firefox-esr', not 'firefox'
     - Ubuntu's 'chromium' deb is a snap-shim wrapper that
       requires snapd, which Armbian doesn't ship; the shim
       fails at runtime
     - 'chromium' isn't built for riscv64 in either Debian
       or Ubuntu
     - 'firefox' isn't built for noble/plucky riscv64 either
   New map:
     bookworm:  amd64/arm64/armhf -> chromium
     trixie:    amd64/arm64/armhf -> chromium
                riscv64           -> firefox-esr
     noble:     all archs         -> epiphany-browser
     plucky:    all archs         -> epiphany-browser
   epiphany-browser (GNOME Web) is small, native, real-deb,
   and present on every Ubuntu arch.

3) tier_overrides schema gains a per-release-per-arch layer.
   Old form (per-arch only) handled blender/inkscape on armhf
   etc. New form keeps that AND adds a 'releases.<release>.
   architectures.<arch>' nesting for transient holes (e.g.
   'loupe' missing on bookworm because GNOME 43 didn't have it).

4) Parser walks tier_overrides at every tier step in the walk,
   not just at the target tier. Without this, a hole declared
   at the mid tier (e.g. 'loupe' missing on bookworm) would be
   honoured for mid installs but ignored for full installs,
   because the full install pulls in mid packages and then
   only applies full-tier overrides. The fix is to apply each
   tier's overrides at the same step as that tier's packages.

5) Per-DE tier_overrides also work. Verified on:
     - kde-plasma drops gnome-text-editor / file-roller / loupe
       at mid (overrides common-tier additions to use KDE
       equivalents)
     - gnome drops transmission-gtk at mid (GNOME ships its
       own download integration)

6) common.yaml now carries the holes that apply to every
   desktop:
     mid bookworm */armhf:    remove [loupe]   (no loupe in bookworm)
     mid plucky armhf:        remove [loupe]   (dropped on plucky/armhf)
     full bookworm armhf:     remove [thunderbird] (missing on armhf)
     full trixie armhf:       remove [thunderbird]
     full noble all archs:    remove [thunderbird] (snap-shim only)
     full plucky all archs:   remove [thunderbird]

Verified all 384 (DE x release x arch x tier) combinations
parse cleanly. Verified the holes are honoured by spot-checking
the resolved package list for every release/arch combination
that has a known hole.
The dialog menu calls 'module_desktops status de=<name>' from
the `condition` field of every entry. With ~21 conditional
desktop entries (7 supported DEs × 3 install tiers = 21
install entries), every menu render fired status 21 times in
a row. The previous commit had status print "not installed"
on the not-installed path, which produced 21 "not installed"
lines stacked above the actual menu output.

Fix: print nothing on the not-installed path. The exit code
(1) is the only thing the menu's condition gate looks at.
Installed callers that want the tier name still get it via
stdout (echo of the .tier file contents) and a 0 exit code.
The 'login' command's gdm3 check used an unanchored regex
matching the substring 'AutomaticLoginEnable\s*=\s*true' anywhere
on a line. The stock /etc/gdm3/custom.conf template that Ubuntu
noble's gdm3 ships contains:

  # Enabling automatic login
  #  AutomaticLoginEnable = true
  #  AutomaticLogin = user1

The commented sample line matches the substring, so 'login' would
return 0 ('autologin enabled') on every fresh noble install where
the user had never touched autologin. The user-visible symptom:
the dialog menu's "Disable autologin" entry was always visible
(because its condition uses 'login' returning 0) and "Enable
autologin" was never visible — even though autologin was actually
off.

Anchor the regex at line start so the comment lines don't match.
Use the same character class form ([[:space:]]) as the manual
sed call for consistency.

Verified across four cases:
  - stock template with commented sample -> no match (correct)
  - enabled config 'AutomaticLoginEnable = true' -> match
  - disabled config 'AutomaticLoginEnable = false' -> no match
  - no whitespace 'AutomaticLoginEnable=true' -> match
The 'status' command was printing the installed tier name on
stdout to make it useful from the CLI. But status is also called
from every dialog menu entry's `condition` field, dozens of
times per render — and any stdout output leaks into the dialog,
producing strings of "full / full / full / mid / ..." between
the menu items.

Split the responsibility:

  - status      : silent exit-code query. Returns 0 if installed,
                  1 if not. No output on either path. This is
                  what menu condition gates use.
  - tier (NEW)  : value-returning getter. Prints the installed
                  tier name (minimal/mid/full) when installed,
                  prints "not installed" otherwise. Use from
                  the CLI when you want the actual value.

This is the same fix pattern as the previous "not installed"
silence — extending it from one path to the other.
Add a way to upgrade or downgrade an installed desktop's tier
from the dialog menu. Two new bash commands plus 18 new menu
entries (6 supported DEs x 3 target tiers).

New bash commands
-----------------
at-tier  — silent gate. Exit 0 if a desktop is installed AND its
           current tier marker matches the given target. Used
           by the menu's `condition` field to hide the
           "Change to <tier>" entry that matches the currently
           installed tier (so the user only sees the OTHER two
           tiers as switch options).
           Pure exit-code query, no stdout output, same shape
           as `status` and `login`.

set-tier — direction-agnostic tier change. Reads the current
           tier from the marker, dispatches to upgrade or
           downgrade based on whether the target is higher or
           lower. Refuses with a friendly message if the
           desktop isn't installed or already at the target.
           Used by the menu's `command` field so a single
           button can switch in either direction without the
           menu having to know the current state.

Internally set-tier reuses _module_desktops_change_tier (the
upgrade/downgrade implementation) — set-tier is just a
front-end that figures out the direction.

Menu entries
------------
For each of the 6 supported DEs in the menu (cinnamon, gnome,
mate, i3-wm, kde-plasma, xfce), 3 entries:

  *07  Change <DE> to minimal
  *08  Change <DE> to mid
  *09  Change <DE> to full

Each entry's condition is

  module_desktops status de=<X> && ! module_desktops at-tier de=<X> tier=<target>

so the entry is visible only when the DE is installed AND not
already at the target tier. With xfce installed at minimal,
the user sees XFCE08 (Change XFCE to mid) and XFCE09 (Change
XFCE to full), but NOT XFCE07 (Change to minimal). After
running XFCE08, the menu re-renders showing XFCE07 and XFCE09
instead.

ID slot allocation
------------------
Existing desktop slot usage:
  *01 install minimal
  *02 uninstall
  *03 enable autologin
  *04 disable autologin
  *05 install mid
  *06 install full

New slots:
  *07 change to minimal
  *08 change to mid
  *09 change to full

Verified across the file: zero duplicate IDs after the change.

KDE Neon is intentionally not in the list — it's status:
unsupported and only has its single KDEN01 install entry.
xmonad and enlightenment also aren't in the list because they
don't have install entries in the menu yet either; adding
them is a separate UX call.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

This pull request introduces a tiered desktop installation and auditing system. It adds a GitHub Actions workflow that periodically audits desktop configurations against supported releases and package availability, using Claude to propose fixes. The desktop module system is refactored from flat package lists to a tier-based model (minimal, mid, full), with new shell functions supporting tier upgrades, downgrades, and tier transitions. Desktop YAML files are restructured to define packages per tier, while the configuration menu is expanded with tier-specific install and tier-change options. Supporting scripts are updated to parse and apply tier selections, and two new Python tools implement package availability checking and Claude-driven remediation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'desktops: tier system, install/remove correctness, and a pile of small fixes' directly summarizes the three main layers of changes: tier system introduction, install/remove bug fixes, and additional cleanups.
Description check ✅ Passed The description comprehensively covers the PR's objectives across three layers: bug fixes, audit cleanup, and the tier system. It explains the motivation, changes, testing, and scope—clearly related to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch smallfixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added size/large PR with 250 lines or more 05 Milestone: Second quarter release labels Apr 11, 2026
…n empty

CI was failing on every desktop test with two errors:

  expr: non-integer argument
  Error: specify tier=minimal|mid|full

The first is a pre-existing init bug — `apt-cache policy
zfs-dkms` returns nothing in test containers (no zfs-dkms in
the apt sources), ZFS_DKMS_VERSION is empty, and the
previous-version probe pipes empty input through `expr` which
errors out. Guard the probe: only run it when
ZFS_DKMS_VERSION matches the expected N.N.N format.

The second is the breaking change from the tier system PR
(install now requires tier=). Update every desktop test to
pass `tier=full` for the supported DEs (xfce, mate, cinnamon,
gnome, i3-wm, xmonad, enlightenment, kde-plasma) so the test
exercises the largest install path. Unsupported DEs (budgie,
deepin, kde-neon — also disabled or not really meant to work)
keep `tier=minimal` since their YAML doesn't expect the
common-tier mid/full extras.

KDE Neon specifically has a comment explaining why it stays
at minimal: its YAML only declares a tiers.minimal block,
and pushing common's mid/full extras (chromium, libreoffice,
gimp, etc.) into it would just add cruft a kde-neon user
would not expect.
Two line continuations were aligned with spaces instead of
tabs, which trips the editorconfig-checker step in CI:

  module_desktops.sh:140  Wrong indentation type (spaces instead of tabs)
  module_desktops.sh:199  Wrong indentation type (spaces instead of tabs)

Both were the same pattern: an `if [[ ... \\` continuation
where the next line was indented to align under the opening
condition with literal spaces. Reformat them to put the
operator at the start of a tab-indented continuation line
instead, which both reads cleanly and uses tabs only.

No behavior change; bash parses both forms identically.
fonts-ubuntu is an Ubuntu-only package — Debian doesn't ship
it. Five DE YAMLs (xfce, mate, cinnamon, kde-plasma, i3-wm)
list it in their tiers.minimal.packages, and on Debian
bookworm/trixie that produces

  E: Package 'fonts-ubuntu' has no installation candidate

aborting the entire desktop install.

Add a tier_overrides.minimal block to common.yaml that strips
fonts-ubuntu on every Debian release (bookworm and trixie,
all arches). The Ubuntu releases (noble, plucky) keep it so
the Ubuntu branding font stays on Ubuntu installs.

Doing this in common.yaml means we don't have to touch each
of the five DE YAMLs individually — the parser walks
common's tier_overrides for every DE in addition to the
DE's own tier_overrides.

Verified across the matrix: bookworm and trixie on
amd64/arm64/armhf (and trixie on riscv64) drop fonts-ubuntu
for all five DEs; noble and plucky keep it. The fallback
font stack on Debian (fonts-noto, fonts-dejavu, etc.) is
pulled in transitively by the GTK / DE packages.
Adds a scheduled GitHub Actions workflow that audits the desktop
YAML matrix for two kinds of staleness and proposes fixes via the
Anthropic API.

Pieces
------

  tools/desktops/audit.py
    Deterministic auditor. Checks out armbian/build alongside
    configng (from CI; from local CLI when run by hand) and
    cross-references three things:
      - releases declared in armbian/build's config/distributions/
        (with their support status — 'supported', 'csc', 'eos', ...)
      - the 'releases:' blocks declared in every DE YAML
      - the per-(release, arch, tier) resolved DESKTOP_PACKAGES set
        from parse_desktop_yaml.py
    For each DESKTOP_PACKAGES entry, fetches packages.debian.org or
    packages.ubuntu.com and decides whether the package actually
    exists in that (release, arch). Outputs a JSON report listing
    'missing_releases' (build supports it, no DE YAML covers it,
    not EOS) and 'package_holes' (DESKTOP_PACKAGES names a package
    that the upstream archive doesn't ship for that combination).
    Pure logic, no LLM.

  tools/desktops/audit_apply.py
    Reads the JSON report and hands it to Claude via the Anthropic
    Python SDK. Claude is given file Read/Write tool access scoped
    strictly to tools/modules/desktops/yaml/ and is told to propose
    minimal edits to common.yaml's tier_overrides for package
    holes and to add release blocks to per-DE YAMLs for missing
    releases. After Claude finishes, post-edit validation re-parses
    every YAML and spot-checks the parser to ensure nothing was
    broken. Short-circuits on no findings (so 'no work' runs cost
    nothing in API tokens). Also has --dry-run mode that prints the
    prompt without calling the API, useful for local testing.

  .github/workflows/maintenance-desktop-audit.yml
    Schedule: Mondays 06:00 UTC. Also workflow_dispatch with
    optional tier / release filter inputs and a dry_run toggle.
    Job: checkout both repos, run audit.py, surface findings as
    a job summary (markdown tables), upload the report as an
    artifact, run audit_apply.py if there's anything actionable,
    open or update a draft PR via peter-evans/create-pull-request
    on a fixed branch (bot/desktop-matrix-audit) so weekly runs
    update the existing PR rather than spamming new ones.

Operational notes
-----------------

  - The Claude apply step is gated on actionable findings AND on
    ANTHROPIC_API_KEY being present. Without the secret it logs a
    warning and exits cleanly, so the workflow can run as a smoke
    test without any API access.
  - Sandboxed file access: Claude can only read files inside the
    configng checkout (path traversal blocked) and can only write
    files inside tools/modules/desktops/yaml/. No bash, no network.
  - Token cap: --max-tokens 50000 by default, configurable.
  - PR is opened as a draft so a human can review before merging.
    Labels applied: bot, desktops, documentation.

Local validation
----------------

Tested locally against the live build + configng checkouts:

  python3 tools/desktops/audit.py \\
      --build-repo /home/igorp/Development/build \\
      --configng-repo . \\
      --skip-network --output /tmp/r.json

Found 5 missing CSC releases (forky, jammy, questing, resolute,
sid) and 0 package holes — exactly what's expected given the
current YAML matrix.
@github-actions github-actions bot added GitHub Actions GitHub Actions code GitHub GitHub-related changes like labels, templates, ... labels Apr 11, 2026
@igorpecovnik igorpecovnik merged commit 1bfa186 into main Apr 11, 2026
37 of 38 checks passed
@igorpecovnik igorpecovnik deleted the smallfixes branch April 11, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

05 Milestone: Second quarter release GitHub Actions GitHub Actions code GitHub GitHub-related changes like labels, templates, ... size/large PR with 250 lines or more Unit Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant