From 391a28f30a8fb640266d27a471c7cbe22f752bc9 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Fri, 10 Apr 2026 08:27:38 +0200 Subject: [PATCH 01/13] Add developer guide for the desktop submodule Technical reference for tools/modules/desktops/ in configng: component map, YAML schema, parse_desktop_yaml.py CLI surface, all bash module functions, install/remove lifecycle, container/CI behavior, "adding a new desktop" walkthrough, and security notes (path traversal guard, shell-escape, GPG keyring fetch). Filed under ARMBIAN BUILD FRAMEWORK alongside Extensions, since it documents an internal API rather than end-user steps. --- docs/Developer-Guide_Desktops.md | 394 +++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 395 insertions(+) create mode 100644 docs/Developer-Guide_Desktops.md diff --git a/docs/Developer-Guide_Desktops.md b/docs/Developer-Guide_Desktops.md new file mode 100644 index 00000000..1963ca7f --- /dev/null +++ b/docs/Developer-Guide_Desktops.md @@ -0,0 +1,394 @@ +# Desktops + +Technical reference for the desktop submodule of `armbian-config` (the [configng](https://github.com/armbian/configng) repository), under `tools/modules/desktops/`. This guide is aimed at developers who want to add a new desktop environment, modify the install/remove pipeline, or integrate the YAML-driven desktop API from another tool. + +End-user instructions for installing a desktop with `armbian-config` live in the [Armbian Config](User-Guide_Armbian-Config.md) section. + +## Overview + +The desktop submodule replaces hand-rolled per-distro install scripts with a single YAML-driven pipeline. Each desktop environment is described by one YAML file in `tools/modules/desktops/yaml/`. A Python helper parses the YAML and emits bash-compatible variables that the rest of the module evaluates and acts on. + +The submodule provides: + +- **Install / remove** of a desktop environment, including its display manager, custom APT repositories, branding, AppImage extras, group memberships, and skel sync. +- **Auto-login** management for `gdm3`, `sddm`, and `lightdm`. +- **Support queries** that report which desktops are available for a given (release, architecture) pair as TSV or JSON. +- **Container/CI awareness** so the same code path can be used inside Docker without trying to start a display manager. + +## Component map + +```text +tools/modules/desktops/ +├── module_desktops.sh # main dispatcher: install/remove/auto/manual/... +├── module_desktop_yamlparse.sh # bash wrapper around the YAML parser +├── module_desktop_supported.sh # arch/release support check +├── module_desktop_repo.sh # custom APT repo + GPG keyring setup +├── module_desktop_branding.sh # wallpapers, greeters, skel, postinst hook +├── module_desktop_getuser.sh # detects first regular user +├── module_update_skel.sh # propagate /etc/skel into existing $HOME +├── module_appimage.sh # AppImage helper (used for armbian-imager) +│ +├── scripts/ +│ └── parse_desktop_yaml.py # YAML → bash-eval variables (or TSV/JSON listings) +│ +├── yaml/ +│ ├── common.yaml # packages installed for every DE +│ └── .yaml # per-desktop definition +│ +├── postinst/.sh # optional DE-specific post-install hook +├── greeters/{lightdm,sddm}/ # greeter configs and SDDM themes +├── branding/ +│ ├── wallpapers/ # /usr/share/backgrounds/armbian +│ ├── wallpapers-lightdm/ # /usr/share/backgrounds/armbian-lightdm +│ ├── icons/ # /usr/share/icons/armbian +│ ├── pixmaps/ # /usr/share/pixmaps/armbian +│ └── armbian.xml # GNOME background-properties +└── skel/ # files copied into /etc/skel +``` + +Every shell file is loaded by configng's module loader, which exposes them as bash functions in the running shell. `script_dir` points at the configng install root and is used to resolve paths relative to the desktops directory. + +## Data flow + +```text + CLI: armbian-config desktop install de=xfce + │ + ▼ + module_desktops install de=xfce + │ + │ 1. resolves user via module_desktop_getuser + │ 2. parses xfce.yaml via module_desktop_yamlparse → DESKTOP_* vars + │ 3. sets up custom repo via module_desktop_repo + │ 4. apt install $DESKTOP_PACKAGES + $DESKTOP_DM + │ 5. apt remove $DESKTOP_PACKAGES_UNINSTALL + │ 6. installs branding via module_desktop_branding + │ 7. installs Armbian Imager AppImage + │ 8. adds user to sudo/audio/video/... groups + │ 9. propagates /etc/skel via module_update_skel + │ 10. starts display-manager (skipped in containers) + │ 11. enables auto-login via `module_desktops auto` + ▼ + desktop ready +``` + +The Python helper is the single source of truth for what packages get installed for a given (desktop, release, arch) combination. The bash side never reads YAML directly. + +## YAML schema + +Each desktop is defined in a single YAML file under `tools/modules/desktops/yaml/`. Filename without `.yaml` is the canonical desktop name (`de_name`). + +### Top-level fields + +| Field | Type | Required | Description | +|---|---|---|---| +| `name` | string | informational | Human-readable name. | +| `description` | string | informational | One-line summary, exposed via `DESKTOP_DESC`. | +| `display_manager` | string | yes | Greeter package: `gdm3`, `sddm`, `lightdm`, or `none`. | +| `status` | string | yes | `supported` or `unsupported`. Reported via `DESKTOP_STATUS`. Affects only labelling — does not block install. | +| `packages` | list | yes | DE-specific packages. The first element is treated as the **primary package** and used by `module_desktops status` to detect installation. | +| `packages_uninstall` | list | optional | Packages to purge after the install (junk that the DE metapackage pulls in). | +| `releases` | mapping | yes | Per-release overrides keyed by release codename (`bookworm`, `trixie`, `noble`, `plucky`, ...). | +| `repo` | mapping | optional | Custom APT repository, see below. | + +### Per-release block (`releases.`) + +| Field | Type | Description | +|---|---|---| +| `architectures` | list | Architectures supported on this release. Used to compute `DESKTOP_SUPPORTED`. | +| `packages` | list | Extra packages added on top of the top-level `packages`. | +| `packages_remove` | list | Packages filtered out of the merged install list (used to drop a top-level package on a specific release). | +| `packages_uninstall` | list | Packages purged after install on this release only. | + +### Custom repository block (`repo`) + +| Field | Type | Description | +|---|---|---| +| `url` | string | Base URL for `deb [signed-by=...] main`. | +| `key_url` | string | URL to the GPG key (ASCII-armored). | +| `keyring` | string | Path to the dearmored keyring file, e.g. `/usr/share/keyrings/neon.gpg`. | + +### Example + +```yaml title="yaml/xfce.yaml" +name: xfce +description: "XFCE - lightweight and fast desktop" +display_manager: lightdm +status: supported + +packages: + - xfce4 + - xfce4-goodies + - lightdm + - slick-greeter + # ... + +packages_uninstall: + - ristretto + - xfburn + +releases: + trixie: + architectures: [arm64, amd64, armhf, riscv64] + packages: + - pipewire-audio + - pipewire-pulse + - wireplumber + packages_remove: + - pulseaudio + - pulseaudio-module-bluetooth +``` + +```yaml title="yaml/kde-neon.yaml — with custom repo" +name: kde-neon +description: "KDE Neon - latest Plasma from KDE repos (Ubuntu only)" +display_manager: sddm +status: unsupported +repo: + url: "http://archive.neon.kde.org/testing" + key_url: "https://archive.neon.kde.org/public.key" + keyring: "/usr/share/keyrings/neon.gpg" + +packages: + - neon-desktop + - sddm + # ... + +releases: + noble: + architectures: [arm64, amd64] +``` + +### `common.yaml` + +Packages listed in `common.yaml` are added to every desktop install. Keep it minimal — anything desktop-specific belongs in the per-desktop file. + +```yaml title="yaml/common.yaml" +name: common +description: "Common packages for all desktop environments" +packages: + - adwaita-icon-theme + - cups + - dconf-cli + - profile-sync-daemon + - terminator + - upower +``` + +## Python helper: `parse_desktop_yaml.py` + +Single-purpose CLI that bash modules invoke via `python3`. All YAML parsing and validation happens here so the bash side stays free of YAML logic. + +### Usage + +```bash +# Parse one desktop and emit DESKTOP_* shell variables +parse_desktop_yaml.py + +# List all desktops as TSV (namestatussupportedarchs) +parse_desktop_yaml.py --list + +# Same as --list but JSON-formatted +parse_desktop_yaml.py --list-json +``` + +### Variables emitted (per-desktop mode) + +All values are double-quoted and shell-escaped via `shell_escape()` (escapes `\`, `"`, `$`, and `` ` ``), so the bash caller can safely `eval` the output. + +| Variable | Source | Notes | +|---|---|---| +| `DESKTOP_PACKAGES` | merged: `common.yaml` + top-level `packages` + release `packages` − release `packages_remove` | Space-separated, ready to feed to `apt install`. | +| `DESKTOP_PACKAGES_UNINSTALL` | top-level `packages_uninstall` + release `packages_uninstall` | Space-separated. | +| `DESKTOP_PRIMARY_PKG` | first element of top-level `packages` | Used by `module_desktops status` for `dpkg -l` checks. | +| `DESKTOP_DM` | `display_manager`, default `lightdm` | | +| `DESKTOP_STATUS` | `status`, default `unsupported` | | +| `DESKTOP_SUPPORTED` | `yes` if `arch` is in the release's `architectures` and `release` is a key in `releases`, else `no` | | +| `DESKTOP_DESC` | `description`, default `de_name` | | +| `DESKTOP_REPO_URL` | `repo.url` | Only emitted when `repo:` exists. | +| `DESKTOP_REPO_KEY_URL` | `repo.key_url` | Only emitted when `repo:` exists. | +| `DESKTOP_REPO_KEYRING` | `repo.keyring` | Only emitted when `repo:` exists. | + +### Error handling and validation + +The parser is strict about top-level structure but tolerant of malformed sub-nodes: + +- **Path traversal guard** — `de_name` is resolved against `yaml_dir` via `os.path.realpath`/`commonpath`. Anything outside the directory (`../...`, absolute paths, symlink escapes) is rejected with `Error: invalid desktop name ''` and exit 1. +- **Required structural checks** — top-level YAML must be a mapping; `common.yaml` and per-desktop `packages` must be lists. Failures print a clear `Error: ...` and exit 1. +- **Tolerant normalization** — `releases`, per-release blocks, `architectures`, release-level package lists, top-level `packages_uninstall`, and `repo` all pass through `_as_dict` / `_as_list` helpers. Wrong-typed nodes coerce to safe empty defaults (`{}` or `[]`) instead of raising `AttributeError` or doing surprising substring matches like `arch in "arm64"`. + +### `--list` / `--list-json` mode + +Iterates every `*.yaml` (excluding `common.yaml`), parses each one, and prints **only entries supported on the requested (release, arch)**. Used by `module_desktops install` to show available desktops on error and by `module_desktops supported` to expose a machine-readable catalog. + +## Bash module API + +All functions are loaded by configng's module loader. They share global state (`DESKTOP_*` variables, `script_dir`, `DISTROID`) — call sites must follow the documented order. + +### `module_desktops [de=] [arch=] [release=]` + +Top-level dispatcher. The `de=`, `arch=`, `release=` arguments are parsed positionally from `$@`. + +| Command | Behavior | Required args | +|---|---|---| +| `install` | Full install pipeline (see [Lifecycle](#lifecycle-install)). | `de=` | +| `remove` | Disables auto-login, stops the display manager, purges the DM and primary package. | `de=` | +| `disable` | `systemctl stop && disable display-manager`. | — | +| `enable` | `systemctl enable && start display-manager`. | — | +| `status` | Returns 0 if `DESKTOP_PRIMARY_PKG` is `dpkg -l` installed. | `de=` | +| `auto` | Configures auto-login for `DESKTOP_DM` (gdm3/sddm/lightdm). | `de=` | +| `manual` | Reverts auto-login. | `de=` | +| `login` | Returns 0 if auto-login is currently configured. | `de=` | +| `supported`| With `de=`: prints `true`/`false`. Without `de=`: prints JSON catalog of supported desktops. | optional | +| `help` | Shows help and exits. | — | + +#### Auto-login files written + +| Display manager | File | +|---|---| +| `gdm3` | `/etc/gdm3/custom.conf` (or `daemon.conf` on `trixie`/`forky`) | +| `sddm` | `/etc/sddm.conf.d/autologin.conf` | +| `lightdm` | `/etc/lightdm/lightdm.conf.d/22-armbian-autologin.conf` | + +### `module_desktop_yamlparse [arch] [release]` + +Wraps `parse_desktop_yaml.py`. Resets all `DESKTOP_*` globals, runs the helper, and `eval`s its stdout. Returns 1 on parse failure (with the parser's stderr surfaced). + +`arch` defaults to `dpkg --print-architecture`. `release` defaults to `$DISTROID`. + +```bash +module_desktop_yamlparse xfce +echo "$DESKTOP_PRIMARY_PKG" # → xfce4 +echo "$DESKTOP_SUPPORTED" # → yes / no +``` + +### `module_desktop_yamlparse_list [arch] [release]` + +Calls the parser with `--list` and prints TSV to stdout. Used to assemble the "Available: ..." hint shown when `install` is invoked without `de=`. + +### `module_desktop_supported [arch] [release]` + +Convenience wrapper around `module_desktop_yamlparse` that returns 0/1 based on `DESKTOP_SUPPORTED`. Suppresses parser stderr — meant for predicates and CI gates. + +### `module_desktop_repo ` + +Sets up a custom APT source. Must be called **after** `module_desktop_yamlparse` because it consumes `DESKTOP_REPO_URL`, `DESKTOP_REPO_KEY_URL`, `DESKTOP_REPO_KEYRING`. + +Behavior: + +1. Validates `de_name` against `^[a-zA-Z0-9._-]+$` (defense in depth — the YAML parser already blocks traversal). +2. `curl --retry 3 --connect-timeout 10 --max-time 30 ... | gpg --dearmor` writes the keyring. Pipefail is set so a curl failure is surfaced. +3. Verifies the keyring is non-empty before proceeding (catches HTML error pages dearmoring to a zero-byte file). +4. Writes `/etc/apt/sources.list.d/.list` with `deb [signed-by=] $DISTROID main`. + +A no-op if the YAML has no `repo:` block. + +### `module_desktop_branding ` + +Copies branding assets and runs the optional postinst hook. Idempotent — every step is guarded with `[[ -d ... ]]`. + +| Source (under `tools/modules/desktops/`) | Destination | +|---|---| +| `greeters/lightdm/` | `/etc/armbian/lightdm/` and mirrored to `/etc/lightdm/` | +| `skel/` | `/etc/skel/` | +| `branding/wallpapers/*.jpg` | `/usr/share/backgrounds/armbian/` | +| `branding/wallpapers-lightdm/*.jpg` | `/usr/share/backgrounds/armbian-lightdm/` | +| `branding/icons/*` | `/usr/share/icons/armbian/` | +| `branding/pixmaps/*` | `/usr/share/pixmaps/armbian/` | +| `branding/armbian.xml` | `/usr/share/gnome-background-properties/` | +| `greeters/sddm/themes/*` | `/usr/share/sddm/themes/` (only when `DESKTOP_DM=sddm`) | +| `postinst/.sh` | Executed via `bash` (skipped inside containers/CI) | + +### `module_desktop_getuser` + +Returns the first non-root, non-system user with a real login shell. Prefers `$SUDO_USER` if set and not root, otherwise scans `/etc/passwd` for the first entry with `1000 ≤ uid < 65534` and a shell that does not match `nologin|false`. Exits 1 if none is found. + +### `module_update_skel install` + +Walks `getent passwd`, and for every regular user (`1000 ≤ uid < 65534`, home directory exists, not root) copies any file present in `/etc/skel` but missing in the user's home, fixing ownership to `uid:gid`. Existing files are never overwritten. + +### `module_appimage app=` + +Used by `module_desktops install` to install `armbian-imager`. The internal `APPIMAGE_REPO` registry maps logical app names to GitHub `owner/repo` slugs and downloads the appropriate architecture-suffixed AppImage from the latest release. + +## Lifecycle: install {#lifecycle-install} + +The install pipeline in `module_desktops install` is intentionally linear and idempotent-friendly. + +```text +1. Resolve target user module_desktop_getuser +2. Parse YAML module_desktop_yamlparse $de +3. Validate package list exit if DESKTOP_PACKAGES / DESKTOP_PRIMARY_PKG empty +4. Warn on unsupported DESKTOP_SUPPORTED != yes → stderr warning, continue +5. Suppress encfs prompt debconf-set-selections +6. Configure custom repo module_desktop_repo $de +7. apt update pkg_update +8. apt install desktop pkgs pkg_install $DESKTOP_PACKAGES +9. apt install + register DM /etc/X11/default-display-manager +10. Purge unwanted packages apt-get remove --purge $DESKTOP_PACKAGES_UNINSTALL +11. Install branding module_desktop_branding $de +12. Install Armbian Imager module_appimage install app=armbian-imager +13. Add user to groups sudo netdev audio video dialout plugdev input bluetooth systemd-journal ssh +14. Profile sync daemon (psd) touch ~/.activate_psd, sudoers entry +15. Sync skel to existing users module_update_skel install +16. Start display manager skipped if _desktop_in_container +17. Enable auto-login module_desktops auto de=$de +``` + +The remove pipeline (`module_desktops remove`) reverses the user-visible parts: disables auto-login, stops the display manager, purges the DM and primary package (`apt`'s autoremove handles dependencies), and removes the Armbian Imager AppImage. It does **not** uninstall the full `DESKTOP_PACKAGES` set or undo branding — both are deliberate to avoid removing things the user may now depend on. + +## Container and CI awareness + +`_desktop_in_container` returns true when any of the following holds: + +- `/.dockerenv` exists +- `/run/.containerenv` exists +- `$CI` is set +- `$GITHUB_ACTIONS` is set + +Inside a container the install pipeline still does packages, branding, and skel work, but **skips**: + +- Stopping or starting any display manager +- Restarting the display manager after auto-login changes +- Running per-desktop `postinst/.sh` hooks + +This makes the same code path usable for image preseeding inside Docker without needing parallel "container mode" branches. + +## Adding a new desktop + +1. **Create the YAML.** Drop a new file at `tools/modules/desktops/yaml/.yaml` following the [schema](#yaml-schema). Minimum required fields: `display_manager`, `status`, `packages`, and at least one entry under `releases.` with an `architectures` list. +2. **(Optional) Custom repo.** Add a `repo:` block if the DE is not in the distro's default repositories. Pin the keyring path under `/usr/share/keyrings/`. +3. **(Optional) Postinst hook.** Drop `tools/modules/desktops/postinst/.sh` for any per-DE configuration that has to run after `apt install`. Container/CI runs are skipped automatically. +4. **(Optional) Branding overrides.** Branding lives in shared directories, so most desktops do not need any per-DE assets — only add files when the DE needs something different. +5. **Smoke test the parser:** + + ```bash + cd configng + python3 tools/modules/desktops/scripts/parse_desktop_yaml.py \ + tools/modules/desktops/yaml trixie arm64 + ``` + + All `DESKTOP_*` variables should print, and `DESKTOP_SUPPORTED="yes"` for any (release, arch) pair you listed in the YAML. + +6. **List-mode sanity check:** + + ```bash + python3 tools/modules/desktops/scripts/parse_desktop_yaml.py \ + tools/modules/desktops/yaml --list trixie arm64 + ``` + + Your new desktop should appear in the TSV output for the (release, arch) combinations you declared. + +7. **End-to-end test** in a disposable VM or container with `armbian-config desktop install de=`. + +## Security notes + +- **Path traversal**: `de_name` flows from CLI input into `os.path.join(yaml_dir, f"{de_name}.yaml")`. The Python helper resolves both sides via `os.path.realpath` and rejects anything outside `yaml_dir` (handles `..`, absolute paths, and symlink escapes). `module_desktop_repo` additionally validates `de_name` against `^[a-zA-Z0-9._-]+$` before writing `/etc/apt/sources.list.d/.list`. +- **Shell injection**: all values emitted by the Python helper pass through `shell_escape()` (escapes `\`, `"`, `$`, `` ` ``) so the bash caller can `eval` the output safely even when YAML strings contain shell metacharacters. +- **GPG keyring fetch**: the `curl | gpg --dearmor` pipeline runs under `set -o pipefail`, with `--retry 3 --connect-timeout 10 --max-time 30`, and a non-empty file check after dearmor. A failed download or an HTML error page does not silently produce an empty keyring. +- **APT sources** are written with `[signed-by=]`, never via `apt-key`. Each desktop's source list lives in its own file (`/etc/apt/sources.list.d/.list`) so removal is a single `rm`. + +## See also + +- [Extensions](Developer-Guide_Extensions.md) — the Armbian build framework's extension system, used by board configs to inject build-time hooks. +- [Armbian Config](User-Guide_Armbian-Config.md) — end-user docs for `armbian-config`. +- [configng repository](https://github.com/armbian/configng) — source for everything described here. diff --git a/mkdocs.yml b/mkdocs.yml index 6b7f2722..93039e7d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -162,6 +162,7 @@ nav: - 'Building with Multipass' : 'Developer-Guide_Building-with-Multipass.md' - 'Building with Docker' : 'Developer-Guide_Building-with-Docker.md' - 'Extensions' : 'Developer-Guide_Extensions.md' + - 'Desktops' : 'Developer-Guide_Desktops.md' - 'ARMBIAN COMMUNITY' : - 'Forums' : 'Community_Forums.md' From d60a6d7ba62bad53c8af819c418222fd151e7d8c Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Sat, 11 Apr 2026 22:23:45 +0200 Subject: [PATCH 02/13] desktops: update developer guide for the tier system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guide was written before the tier system landed in configng. Update it to match the current state of the desktop submodule on smallfixes. Highlights of what changed in the doc: - Add an Overview section explaining the three-tier model (minimal/mid/full), the tier_overrides escape hatch, and the per-install manifest files. - Replace the YAML schema section. The flat top-level `packages:` / `packages_uninstall:` is gone — every DE uses a `tiers:` map keyed by minimal/mid/full now. packages_uninstall belongs under tiers.minimal. Add documentation for `tier_overrides` (per-arch and per-release-per-arch layers) and the `browser:` virtual token map (per-release-per-arch with explanation of why Debian needs firefox-esr and Ubuntu needs epiphany-browser). - Add example KDE Plasma YAML showing how per-DE tier overrides drop common entries (gnome-text-editor, file-roller, loupe) and substitute KDE equivalents (kate). - Document the full common.yaml shape with tiers + browser map + tier_overrides for every release/arch hole the audit found (loupe missing on bookworm, thunderbird missing on noble/plucky as snap-shim, etc.). - Rewrite the parser CLI section. --tier is now mandatory. Document the resolution algorithm step-by-step. - Rewrite the bash module API table. New commands: upgrade, downgrade, set-tier, tier, at-tier. Existing commands changed: install requires tier=, status is now silent on both paths, auto/manual edit gdm config in place, login uses an anchored regex. - Document the manifest files (.packages and .tier under /etc/armbian/desktop/) and what they're used for. - Document the gdm3 daemon.conf-vs-custom.conf branching by ID= (not by codename), the in-place edit behavior, and the anchored login regex. - Add Lifecycle sections for install, remove, and upgrade/downgrade. The install lifecycle now shows fail-safe behavior: pkg_install failures bail before flipping default.target. The remove lifecycle shows the multi-user.target switch and isolate that drops the running session to console without a reboot. - Add a "Common pitfalls" section documenting the three cascade traps we hit during the smallfixes work: packages_uninstall yanking metapackages, daemon.conf vs custom.conf wrong-file branching, and the unanchored login regex matching the noble template's commented sample line. - Update the "Adding a new desktop" walkthrough to test every tier and to show the JSON menu ID slot allocation (01/02/03/04 existing actions, 05/06 mid/full installs, 07/08/09 change-tier entries). The guide is now ~750 lines, up from ~395, reflecting the ~10x growth of the desktop submodule's surface area in this release. --- docs/Developer-Guide_Desktops.md | 573 +++++++++++++++++++++++++------ 1 file changed, 463 insertions(+), 110 deletions(-) diff --git a/docs/Developer-Guide_Desktops.md b/docs/Developer-Guide_Desktops.md index 1963ca7f..54592c7e 100644 --- a/docs/Developer-Guide_Desktops.md +++ b/docs/Developer-Guide_Desktops.md @@ -10,29 +10,46 @@ The desktop submodule replaces hand-rolled per-distro install scripts with a sin The submodule provides: -- **Install / remove** of a desktop environment, including its display manager, custom APT repositories, branding, AppImage extras, group memberships, and skel sync. -- **Auto-login** management for `gdm3`, `sddm`, and `lightdm`. -- **Support queries** that report which desktops are available for a given (release, architecture) pair as TSV or JSON. +- **Tiered install** — every desktop ships at one of three sizes (`minimal`, `mid`, `full`), and users can move between tiers after install via `upgrade`/`downgrade`/`set-tier`. +- **Per-install manifest** — every install records exactly which packages it added so removal and downgrades only undo what they themselves did. +- **Custom APT repositories**, branding, group memberships, and skel sync. +- **Auto-login** management for `gdm3`, `sddm`, and `lightdm`, with non-destructive in-place edits of the underlying config files. +- **Per-release / per-arch package overrides** so the same YAML works across Debian bookworm/trixie and Ubuntu noble/plucky on amd64/arm64/armhf/riscv64 with different package availability. +- **Browser virtual token** that resolves per-release-per-arch (chromium on Debian, epiphany-browser on Ubuntu, firefox-esr on Debian riscv64, …). - **Container/CI awareness** so the same code path can be used inside Docker without trying to start a display manager. +## Tier model + +Every desktop install is run at one of three tiers, in order of inclusion: `minimal -> mid -> full`. Each tier is the union of itself plus all lower tiers, so installing `full` implies `mid` implies `minimal`. Tiers are mandatory; there is no flat "install everything from this YAML" mode. + +| Tier | Contents | Approximate size | +|---|---|---| +| `minimal` | DE itself + 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 | + +The per-tier package lists for `mid` and `full` live in `common.yaml` so every DE inherits them. Per-DE YAMLs override only what they need (e.g. KDE Plasma swaps `gnome-text-editor` for `kate` at the mid tier). + +The currently-installed tier is recorded in `/etc/armbian/desktop/.tier`. The full set of packages installed for a given DE is recorded in `/etc/armbian/desktop/.packages`. + ## Component map ```text tools/modules/desktops/ -├── module_desktops.sh # main dispatcher: install/remove/auto/manual/... -├── module_desktop_yamlparse.sh # bash wrapper around the YAML parser +├── module_desktops.sh # main dispatcher: install/remove/auto/manual/upgrade/downgrade/... +├── module_desktop_yamlparse.sh # bash wrapper around the YAML parser (now takes a tier arg) ├── module_desktop_supported.sh # arch/release support check ├── module_desktop_repo.sh # custom APT repo + GPG keyring setup ├── module_desktop_branding.sh # wallpapers, greeters, skel, postinst hook ├── module_desktop_getuser.sh # detects first regular user -├── module_update_skel.sh # propagate /etc/skel into existing $HOME -├── module_appimage.sh # AppImage helper (used for armbian-imager) +├── module_update_skel.sh # propagate /etc/skel into existing $HOME (with chown -R safety net) +├── module_appimage.sh # AppImage helper (used for armbian-imager via the CLI) │ ├── scripts/ │ └── parse_desktop_yaml.py # YAML → bash-eval variables (or TSV/JSON listings) │ ├── yaml/ -│ ├── common.yaml # packages installed for every DE +│ ├── common.yaml # per-tier defaults installed for every DE; browser map; tier_overrides │ └── .yaml # per-desktop definition │ ├── postinst/.sh # optional DE-specific post-install hook @@ -43,35 +60,41 @@ tools/modules/desktops/ │ ├── icons/ # /usr/share/icons/armbian │ ├── pixmaps/ # /usr/share/pixmaps/armbian │ └── armbian.xml # GNOME background-properties -└── skel/ # files copied into /etc/skel +└── skel/ # files copied into /etc/skel and propagated to existing $HOME ``` -Every shell file is loaded by configng's module loader, which exposes them as bash functions in the running shell. `script_dir` points at the configng install root and is used to resolve paths relative to the desktops directory. +Every shell file is loaded by configng's module loader, which exposes them as bash functions in the running shell. `desktops_dir` points at the desktops directory and is used to resolve paths from any module function. ## Data flow ```text - CLI: armbian-config desktop install de=xfce + CLI: armbian-config --api module_desktops install de=xfce tier=mid │ ▼ - module_desktops install de=xfce + module_desktops install de=xfce tier=mid │ - │ 1. resolves user via module_desktop_getuser - │ 2. parses xfce.yaml via module_desktop_yamlparse → DESKTOP_* vars - │ 3. sets up custom repo via module_desktop_repo - │ 4. apt install $DESKTOP_PACKAGES + $DESKTOP_DM - │ 5. apt remove $DESKTOP_PACKAGES_UNINSTALL - │ 6. installs branding via module_desktop_branding - │ 7. installs Armbian Imager AppImage - │ 8. adds user to sudo/audio/video/... groups - │ 9. propagates /etc/skel via module_update_skel - │ 10. starts display-manager (skipped in containers) - │ 11. enables auto-login via `module_desktops auto` + │ 1. validates tier= (mandatory; minimal|mid|full only) + │ 2. resolves user via module_desktop_getuser + │ 3. parses xfce.yaml at the requested tier via module_desktop_yamlparse + │ → DESKTOP_PACKAGES, DESKTOP_TIER, DESKTOP_DM, DESKTOP_PRIMARY_PKG, ... + │ 4. sets up custom repo via module_desktop_repo + │ 5. apt update + │ 6. apt install $DESKTOP_PACKAGES ← bail on failure (no state changes) + │ 7. apt install $DESKTOP_DM ← bail on failure + │ 8. (Armbian only) install armbian-plymouth-theme if armbian repo present + │ 9. write /etc/armbian/desktop/.packages and .tier + │ 10. apt remove --purge $DESKTOP_PACKAGES_UNINSTALL + │ 11. installs branding via module_desktop_branding + │ 12. adds user to sudo/audio/video/... groups + │ 13. propagates /etc/skel via module_update_skel install (with recursive chown) + │ 14. systemctl start display-manager ← skipped in containers + │ 15. systemctl set-default graphical.target ← only AFTER step 14 succeeds + │ 16. enables auto-login via module_desktops auto ▼ - desktop ready + desktop ready, marker files in /etc/armbian/desktop/ ``` -The Python helper is the single source of truth for what packages get installed for a given (desktop, release, arch) combination. The bash side never reads YAML directly. +The Python helper is the single source of truth for what packages get installed for a given (desktop, release, arch, tier) combination. The bash side never reads YAML directly. ## YAML schema @@ -85,20 +108,53 @@ Each desktop is defined in a single YAML file under `tools/modules/desktops/yaml | `description` | string | informational | One-line summary, exposed via `DESKTOP_DESC`. | | `display_manager` | string | yes | Greeter package: `gdm3`, `sddm`, `lightdm`, or `none`. | | `status` | string | yes | `supported` or `unsupported`. Reported via `DESKTOP_STATUS`. Affects only labelling — does not block install. | -| `packages` | list | yes | DE-specific packages. The first element is treated as the **primary package** and used by `module_desktops status` to detect installation. | -| `packages_uninstall` | list | optional | Packages to purge after the install (junk that the DE metapackage pulls in). | +| `tiers` | mapping | yes | Per-tier package lists, keyed by `minimal`, `mid`, `full`. See [Tier blocks](#tier-blocks). | +| `tier_overrides` | mapping | optional | Per-arch and/or per-release-per-arch package removals (and additions) for tier holes. See [tier_overrides](#tier-overrides). | | `releases` | mapping | yes | Per-release overrides keyed by release codename (`bookworm`, `trixie`, `noble`, `plucky`, ...). | | `repo` | mapping | optional | Custom APT repository, see below. | +### Tier blocks (`tiers.`) + +| Field | Type | Description | +|---|---|---| +| `packages` | list | Packages added at this tier. Combined with `common.yaml`'s same-tier packages and any earlier tiers in the walk. | +| `packages_remove` | list | Packages dropped from the accumulated list at this tier. Use this to remove a `common.yaml` entry that doesn't fit the DE (e.g. KDE Plasma drops `gnome-text-editor` and inserts `kate` at the mid tier). | +| `packages_uninstall` | list | (minimal tier only) Packages purged after install. Used for orthogonal junk that the metapackage pulls in but we want gone (e.g. `apport`, `python3-apport`). **Important**: never list a package that is a hard `Depends:` of any meta package the install ships, or apt's autoremove will cascade and yank a chunk of the desktop. | + +The first DE-specific package that survives all filters becomes `DESKTOP_PRIMARY_PKG`, used by `module_desktops status` for `dpkg -l` checks. It must come from the DE's own `tiers.minimal.packages` block, not from `common.yaml`, otherwise every DE would share the same primary package. + ### Per-release block (`releases.`) +The release block is **orthogonal** to the tier walk: it applies to whatever tier is being installed. Use it for things that vary by release rather than by user choice (e.g. trixie's pulseaudio→pipewire swap, bookworm's `gnome-calculator` addition). + | Field | Type | Description | |---|---|---| | `architectures` | list | Architectures supported on this release. Used to compute `DESKTOP_SUPPORTED`. | -| `packages` | list | Extra packages added on top of the top-level `packages`. | -| `packages_remove` | list | Packages filtered out of the merged install list (used to drop a top-level package on a specific release). | +| `packages` | list | Extra packages added on top of the tier-resolved set. | +| `packages_remove` | list | Packages filtered out of the merged install list. | | `packages_uninstall` | list | Packages purged after install on this release only. | +### tier_overrides + +`tier_overrides` is for **package availability holes**: a tier package that exists on most arches/releases but is missing on one specific combination. The schema has two layers: + +```yaml +tier_overrides: + : + architectures: + : + packages_remove: [...] # apply on this arch in any release + releases: + : + architectures: + : + packages_remove: [...] # apply only on this release+arch combo +``` + +Use the per-arch layer for permanent arch-wide holes (e.g. `blender` always missing on armhf). Use the per-release-per-arch layer for transient holes (e.g. `loupe` missing on bookworm because GNOME 43 didn't have it). The parser walks tier_overrides at every tier step in its walk, so a hole declared at the mid tier is honoured for both `mid` and `full` installs. + +`tier_overrides` can live in `common.yaml` (applies to every DE) or in a per-DE YAML (applies only to that DE). The parser merges common first, then per-DE. + ### Custom repository block (`repo`) | Field | Type | Description | @@ -115,16 +171,19 @@ description: "XFCE - lightweight and fast desktop" display_manager: lightdm status: supported -packages: - - xfce4 - - xfce4-goodies - - lightdm - - slick-greeter - # ... - -packages_uninstall: - - ristretto - - xfburn +tiers: + minimal: + packages: + - xfce4 + - xfce4-goodies + - lightdm + - slick-greeter + # ... + packages_uninstall: + - apport + - python3-apport + - python3-problem-report + - libsnapd-glib-2-1 releases: trixie: @@ -138,6 +197,40 @@ releases: - pulseaudio-module-bluetooth ``` +```yaml title="yaml/kde-plasma.yaml — per-DE tier overrides" +name: kde-plasma +description: "KDE Plasma - feature-rich customizable desktop" +display_manager: sddm +status: supported + +tiers: + minimal: + packages: + - kde-plasma-desktop + - sddm + - konsole + - dolphin + - ark + - gwenview + - okular + # ... + mid: + # KDE already ships ark / gwenview / okular at minimal — drop the + # GTK equivalents that common.yaml's mid tier adds. + packages_remove: + - gnome-text-editor + - file-roller + - loupe + packages: + - kate + full: + # libreoffice-gtk3 vs default LibreOffice integration: KDE picks + # up the breeze style automatically when LibreOffice is installed + # alongside Plasma, so just drop the GTK frontend. + packages_remove: + - libreoffice-gtk3 +``` + ```yaml title="yaml/kde-neon.yaml — with custom repo" name: kde-neon description: "KDE Neon - latest Plasma from KDE repos (Ubuntu only)" @@ -148,10 +241,12 @@ repo: key_url: "https://archive.neon.kde.org/public.key" keyring: "/usr/share/keyrings/neon.gpg" -packages: - - neon-desktop - - sddm - # ... +tiers: + minimal: + packages: + - neon-desktop + - sddm + # ... releases: noble: @@ -160,20 +255,110 @@ releases: ### `common.yaml` -Packages listed in `common.yaml` are added to every desktop install. Keep it minimal — anything desktop-specific belongs in the per-desktop file. +`common.yaml` carries the per-tier defaults that apply to every desktop, the browser substitution table, and any cross-DE `tier_overrides`. Per-DE YAMLs only declare a `tiers` block when they want to add packages on top of common or override common-tier entries. ```yaml title="yaml/common.yaml" name: common -description: "Common packages for all desktop environments" -packages: - - adwaita-icon-theme - - cups - - dconf-cli - - profile-sync-daemon - - terminator - - upower +description: "Packages installed for every desktop, in tiers" + +tiers: + minimal: + packages: + - adwaita-icon-theme + - cups + - dconf-cli + - profile-sync-daemon + - terminator + - upower + mid: + packages: + - browser # virtual — resolved per-arch from `browser:` below + - gnome-text-editor + - gnome-calculator + - loupe + - vlc + - file-roller + - transmission-gtk + full: + packages: + - libreoffice + - libreoffice-gtk3 + - gimp + - inkscape + - thunderbird + - audacity + +browser: + bookworm: + amd64: chromium + arm64: chromium + armhf: chromium + trixie: + amd64: chromium + arm64: chromium + armhf: chromium + riscv64: firefox-esr # 'firefox' does not exist in Debian + noble: + amd64: epiphany-browser # 'chromium' deb is a snap-shim + arm64: epiphany-browser + armhf: epiphany-browser + riscv64: epiphany-browser + plucky: + amd64: epiphany-browser + arm64: epiphany-browser + armhf: epiphany-browser + riscv64: epiphany-browser + +tier_overrides: + mid: + releases: + bookworm: + architectures: + amd64: { packages_remove: [loupe] } # GNOME 43 era — no loupe + arm64: { packages_remove: [loupe] } + armhf: { packages_remove: [loupe] } + plucky: + architectures: + armhf: { packages_remove: [loupe] } # dropped on plucky/armhf + full: + releases: + bookworm: + architectures: + armhf: { packages_remove: [thunderbird] } + trixie: + architectures: + armhf: { packages_remove: [thunderbird] } + noble: + # thunderbird on Ubuntu is a snap-shim that requires snapd + # which Armbian doesn't ship — strip it on every arch. + architectures: + amd64: { packages_remove: [thunderbird] } + arm64: { packages_remove: [thunderbird] } + armhf: { packages_remove: [thunderbird] } + riscv64: { packages_remove: [thunderbird] } + plucky: + architectures: + amd64: { packages_remove: [thunderbird] } + arm64: { packages_remove: [thunderbird] } + armhf: { packages_remove: [thunderbird] } + riscv64: { packages_remove: [thunderbird] } ``` +### Browser virtual token + +The literal string `browser` inside any tier block resolves to a real package name from the `browser:` map at parse time. Lookup order: + +1. `browser..` — most specific +2. `browser.` — per-arch fallback if no per-release entry exists +3. drop the token entirely (silent — install proceeds without a browser rather than failing on a literal `browser` apt name) + +The per-release layer is needed because the same arch can resolve differently across releases: + +- Debian has `firefox-esr` but **no** `firefox` package. +- Ubuntu's `chromium` deb is a snap-shim wrapper that requires `snapd`. Armbian doesn't ship snapd, so the shim is broken at runtime — substitute a real GTK browser instead. `epiphany-browser` (GNOME Web) is small, native deb, and available on every Ubuntu arch. +- `chromium` isn't built for riscv64 in either Debian or Ubuntu. +- `firefox` isn't built for noble/plucky riscv64 either. + ## Python helper: `parse_desktop_yaml.py` Single-purpose CLI that bash modules invoke via `python3`. All YAML parsing and validation happens here so the bash side stays free of YAML logic. @@ -181,14 +366,18 @@ Single-purpose CLI that bash modules invoke via `python3`. All YAML parsing and ### Usage ```bash -# Parse one desktop and emit DESKTOP_* shell variables -parse_desktop_yaml.py +# Parse one desktop at a tier and emit DESKTOP_* shell variables. +# --tier is mandatory. +parse_desktop_yaml.py --tier # List all desktops as TSV (namestatussupportedarchs) parse_desktop_yaml.py --list # Same as --list but JSON-formatted parse_desktop_yaml.py --list-json + +# Print "\t" for every desktop, used by `installed` +parse_desktop_yaml.py --primaries ``` ### Variables emitted (per-desktop mode) @@ -197,68 +386,105 @@ All values are double-quoted and shell-escaped via `shell_escape()` (escapes `\` | Variable | Source | Notes | |---|---|---| -| `DESKTOP_PACKAGES` | merged: `common.yaml` + top-level `packages` + release `packages` − release `packages_remove` | Space-separated, ready to feed to `apt install`. | -| `DESKTOP_PACKAGES_UNINSTALL` | top-level `packages_uninstall` + release `packages_uninstall` | Space-separated. | -| `DESKTOP_PRIMARY_PKG` | first element of top-level `packages` | Used by `module_desktops status` for `dpkg -l` checks. | +| `DESKTOP_PACKAGES` | full tier walk: common minimal/mid/full + DE minimal/mid/full + release `packages` − every layer's `packages_remove` and `tier_overrides` removals. The `browser` virtual token is resolved here. | Space-separated, ready to feed to `apt install`. | +| `DESKTOP_PACKAGES_UNINSTALL` | minimal-tier `packages_uninstall` from common + DE + release | Space-separated. | +| `DESKTOP_PRIMARY_PKG` | first DE-specific package (not from common) that survives all filters | Used by `module_desktops status` for `dpkg -l` checks. | | `DESKTOP_DM` | `display_manager`, default `lightdm` | | | `DESKTOP_STATUS` | `status`, default `unsupported` | | | `DESKTOP_SUPPORTED` | `yes` if `arch` is in the release's `architectures` and `release` is a key in `releases`, else `no` | | | `DESKTOP_DESC` | `description`, default `de_name` | | +| `DESKTOP_TIER` | the requested tier name | Set verbatim from the `--tier` arg. | | `DESKTOP_REPO_URL` | `repo.url` | Only emitted when `repo:` exists. | | `DESKTOP_REPO_KEY_URL` | `repo.key_url` | Only emitted when `repo:` exists. | | `DESKTOP_REPO_KEYRING` | `repo.keyring` | Only emitted when `repo:` exists. | +### Resolution algorithm + +For a given `(de_name, release, arch, tier)`: + +1. Start with empty `packages` and `removes` lists. +2. **Walk tiers** from `minimal` up to the target tier. At each step: + - Merge `common.tiers..packages`, then `de.tiers..packages`, applying each layer's `packages_remove` to filter. + - Apply `common.tier_overrides.` for the (release, arch). + - Apply `de.tier_overrides.` for the (release, arch). +3. **Resolve the `browser` token** to a real package via `common.browser..` (with fallback to `common.browser.`, or drop the token). +4. **Apply the release block**: filter `release..packages_remove`, then add `release..packages`. +5. **Compute `packages_uninstall`** by unioning the minimal-tier `packages_uninstall` from common, DE, and the release block. +6. **Compute `DESKTOP_PRIMARY_PKG`** as the first DE-specific tier-walk package that survived release and per-arch removals. +7. Emit all `DESKTOP_*` variables. + ### Error handling and validation The parser is strict about top-level structure but tolerant of malformed sub-nodes: +- **Mandatory `--tier` arg.** Calling without it prints usage and exits 1. Invalid tier values (`ultra`, etc.) error out with a clear message. - **Path traversal guard** — `de_name` is resolved against `yaml_dir` via `os.path.realpath`/`commonpath`. Anything outside the directory (`../...`, absolute paths, symlink escapes) is rejected with `Error: invalid desktop name ''` and exit 1. -- **Required structural checks** — top-level YAML must be a mapping; `common.yaml` and per-desktop `packages` must be lists. Failures print a clear `Error: ...` and exit 1. -- **Tolerant normalization** — `releases`, per-release blocks, `architectures`, release-level package lists, top-level `packages_uninstall`, and `repo` all pass through `_as_dict` / `_as_list` helpers. Wrong-typed nodes coerce to safe empty defaults (`{}` or `[]`) instead of raising `AttributeError` or doing surprising substring matches like `arch in "arm64"`. +- **Tolerant normalization** — `tiers`, `releases`, `architectures`, `tier_overrides`, `repo`, every list field passes through `_as_dict` / `_as_list` helpers. Wrong-typed nodes coerce to safe empty defaults (`{}` or `[]`) instead of raising `AttributeError` or doing surprising substring matches like `arch in "arm64"`. ### `--list` / `--list-json` mode -Iterates every `*.yaml` (excluding `common.yaml`), parses each one, and prints **only entries supported on the requested (release, arch)**. Used by `module_desktops install` to show available desktops on error and by `module_desktops supported` to expose a machine-readable catalog. +Iterates every `*.yaml` (excluding `common.yaml`), parses each one's release block, and prints **only entries supported on the requested (release, arch)**. Used by `module_desktops install` to show available desktops on error and by `module_desktops supported` to expose a machine-readable catalog. These modes do not require `--tier`. ## Bash module API -All functions are loaded by configng's module loader. They share global state (`DESKTOP_*` variables, `script_dir`, `DISTROID`) — call sites must follow the documented order. +All functions are loaded by configng's module loader. They share global state (`DESKTOP_*` variables, `desktops_dir`, `DISTROID`) — call sites must follow the documented order. -### `module_desktops [de=] [arch=] [release=]` +### `module_desktops [de=] [tier=] [arch=] [release=]` -Top-level dispatcher. The `de=`, `arch=`, `release=` arguments are parsed positionally from `$@`. +Top-level dispatcher. The `de=`, `tier=`, `arch=`, `release=` arguments are parsed positionally from `$@`. | Command | Behavior | Required args | |---|---|---| -| `install` | Full install pipeline (see [Lifecycle](#lifecycle-install)). | `de=` | -| `remove` | Disables auto-login, stops the display manager, purges the DM and primary package. | `de=` | -| `disable` | `systemctl stop && disable display-manager`. | — | -| `enable` | `systemctl enable && start display-manager`. | — | -| `status` | Returns 0 if `DESKTOP_PRIMARY_PKG` is `dpkg -l` installed. | `de=` | -| `auto` | Configures auto-login for `DESKTOP_DM` (gdm3/sddm/lightdm). | `de=` | -| `manual` | Reverts auto-login. | `de=` | -| `login` | Returns 0 if auto-login is currently configured. | `de=` | -| `supported`| With `de=`: prints `true`/`false`. Without `de=`: prints JSON catalog of supported desktops. | optional | -| `help` | Shows help and exits. | — | +| `install` | Full install pipeline (see [Lifecycle](#lifecycle-install)). Bails out cleanly on `pkg_install` failure without changing system state. | `de=`, `tier=` | +| `remove` | Disables auto-login, stops the display manager, purges every package recorded in `.packages`, runs `pkg_clean`, switches `default.target` back to `multi-user`, isolates to multi-user.target so the running session also drops to console. | `de=` | +| `upgrade` | Move an installed desktop to a higher tier. Refuses if the target is the same or lower (use `downgrade`). | `de=`, `tier=` | +| `downgrade` | Move an installed desktop to a lower tier. Removable set is intersected with the install manifest so user-installed packages are never touched. | `de=`, `tier=` | +| `set-tier` | Direction-agnostic tier change — auto-detects upgrade vs downgrade from the current marker. Same arg shape as `upgrade`/`downgrade`. Refuses with a friendly message if not installed or already at the target tier. Used by the dialog menu's "Change to " entries. | `de=`, `tier=` | +| `tier` | Print the installed tier name (`minimal`/`mid`/`full`) on stdout, or `not installed`. Returns 0 if installed, 1 if not. Use this from the CLI when you want the actual tier value. | `de=` | +| `at-tier` | Silent gate: exit 0 if the DE is installed AND its current tier marker matches the given target. Used by dialog menu condition gates. | `de=`, `tier=` | +| `status` | Silent exit-code query. Returns 0 if `DESKTOP_PRIMARY_PKG` is `dpkg -l` installed, 1 if not. **Prints nothing on either path** so it can be used safely from menu condition gates that fire dozens of times per render. | `de=` | +| `disable` | `systemctl stop && disable display-manager`. | — | +| `enable` | `systemctl enable && start display-manager`. | — | +| `auto` | Configures auto-login for `DESKTOP_DM` (gdm3/sddm/lightdm). Edits the gdm config in place — never overwrites the file — so user customization is preserved. | `de=` | +| `manual` | Reverts auto-login. Idempotent. | `de=` | +| `login` | Returns 0 if auto-login is currently configured. Anchored regex; safely ignores commented sample lines in the stock noble `custom.conf`. | `de=` | +| `supported` | With `de=`: prints `true`/`false`. Without `de=`: prints JSON catalog of supported desktops. | optional | +| `installed` | Returns 0 if any desktop is installed (uses cached `--primaries` lookup). | — | +| `help` | Shows help and exits. | — | + +#### Manifest files + +Two files per installed desktop, both under `/etc/armbian/desktop/`: + +| File | Format | Purpose | +|---|---|---| +| `.packages` | newline-separated package names | The exact set of packages newly installed by `module_desktops install` (captured from `apt-get -s install` dry-run via `pkg_install`'s `ACTUALLY_INSTALLED` array). The `remove` path passes this to `pkg_remove`; the `downgrade` path uses it to constrain what may be removed. | +| `.tier` | one line: `minimal`, `mid`, or `full` | Source of truth for the currently-installed tier. Read by `status`, `tier`, `at-tier`, `upgrade`, `downgrade`, `set-tier`. Written by `install` and the tier-change commands. | #### Auto-login files written | Display manager | File | |---|---| -| `gdm3` | `/etc/gdm3/custom.conf` (or `daemon.conf` on `trixie`/`forky`) | -| `sddm` | `/etc/sddm.conf.d/autologin.conf` | -| `lightdm` | `/etc/lightdm/lightdm.conf.d/22-armbian-autologin.conf` | +| `gdm3` | `/etc/gdm3/custom.conf` on Ubuntu, `/etc/gdm3/daemon.conf` on Debian. Branched on `ID=` from `/etc/os-release` (not on release codename — both `bookworm` and `trixie` use `daemon.conf`). The file is edited in place via sed, NOT overwritten — any user customization (`WaylandEnable=false`, etc.) is preserved. | +| `sddm` | `/etc/sddm.conf.d/autologin.conf` (drop-in, non-destructive) | +| `lightdm` | `/etc/lightdm/lightdm.conf.d/22-armbian-autologin.conf` (drop-in, non-destructive) | -### `module_desktop_yamlparse [arch] [release]` +### `module_desktop_yamlparse [arch] [release] [tier]` Wraps `parse_desktop_yaml.py`. Resets all `DESKTOP_*` globals, runs the helper, and `eval`s its stdout. Returns 1 on parse failure (with the parser's stderr surfaced). -`arch` defaults to `dpkg --print-architecture`. `release` defaults to `$DISTROID`. +Defaults: +- `arch` → `dpkg --print-architecture` +- `release` → `$DISTROID` +- `tier` → `minimal` — passed through to the parser's `--tier` arg, so callers that only need `DESKTOP_DM` / `DESKTOP_PRIMARY_PKG` (status checks, autologin paths) don't need to know the actual installed tier. ```bash module_desktop_yamlparse xfce echo "$DESKTOP_PRIMARY_PKG" # → xfce4 -echo "$DESKTOP_SUPPORTED" # → yes / no + +module_desktop_yamlparse xfce arm64 trixie full +echo "$DESKTOP_TIER" # → full +echo "$DESKTOP_PACKAGES" # → minimal + mid + full set, with browser resolved ``` ### `module_desktop_yamlparse_list [arch] [release]` @@ -298,43 +524,106 @@ Copies branding assets and runs the optional postinst hook. Idempotent — every | `greeters/sddm/themes/*` | `/usr/share/sddm/themes/` (only when `DESKTOP_DM=sddm`) | | `postinst/.sh` | Executed via `bash` (skipped inside containers/CI) | +The distributor logo for GNOME Settings → About / KDE Info Center / etc. is **not** installed from here — that file ships from `armbian-base-files` so it stays in sync with the `LOGO=` line in `/etc/os-release`. + ### `module_desktop_getuser` Returns the first non-root, non-system user with a real login shell. Prefers `$SUDO_USER` if set and not root, otherwise scans `/etc/passwd` for the first entry with `1000 ≤ uid < 65534` and a shell that does not match `nologin|false`. Exits 1 if none is found. ### `module_update_skel install` -Walks `getent passwd`, and for every regular user (`1000 ≤ uid < 65534`, home directory exists, not root) copies any file present in `/etc/skel` but missing in the user's home, fixing ownership to `uid:gid`. Existing files are never overwritten. +Walks `getent passwd`, and for every regular user (`1000 ≤ uid < 65534`, home directory exists, not root): + +1. Walks `/etc/skel` with `find -mindepth 1`. For each entry: + - Directory: create at the destination if missing. + - File: copy if the destination doesn't exist; never overwrite. +2. Runs `chown -R "$uid:$gid" "$home/"` as a safety net. + +The recursive `chown` is critical: other package postinst scripts (caja, nemo, gnome-keyring, …) routinely leak root-owned files into the user's `~/.config` directory on first install. Without the recursive chown, those tools refuse to start on first login because they can't write their own config dirs. ### `module_appimage app=` -Used by `module_desktops install` to install `armbian-imager`. The internal `APPIMAGE_REPO` registry maps logical app names to GitHub `owner/repo` slugs and downloads the appropriate architecture-suffixed AppImage from the latest release. +Standalone AppImage helper. The internal `APPIMAGE_REPO` registry maps logical app names (e.g. `armbian-imager`) to GitHub `owner/repo` slugs and downloads the appropriate architecture-suffixed AppImage from the latest release. `module_appimage install` also installs `libfuse2`, `fuse3`, and the `libgles2`/`libegl1`/`libgl1`/`libgl1-mesa-dri` runtime so the AppImage can launch. + +Not called from the desktop install path by default. The `armbian-imager` AppImage is available via `armbian-config --api module_appimage install app=armbian-imager` for users who explicitly want it. ## Lifecycle: install {#lifecycle-install} -The install pipeline in `module_desktops install` is intentionally linear and idempotent-friendly. +The install pipeline in `module_desktops install` is intentionally linear and idempotent-friendly. **Every step that touches system state is gated on the previous step's success.** ```text -1. Resolve target user module_desktop_getuser -2. Parse YAML module_desktop_yamlparse $de -3. Validate package list exit if DESKTOP_PACKAGES / DESKTOP_PRIMARY_PKG empty -4. Warn on unsupported DESKTOP_SUPPORTED != yes → stderr warning, continue -5. Suppress encfs prompt debconf-set-selections -6. Configure custom repo module_desktop_repo $de -7. apt update pkg_update -8. apt install desktop pkgs pkg_install $DESKTOP_PACKAGES -9. apt install + register DM /etc/X11/default-display-manager -10. Purge unwanted packages apt-get remove --purge $DESKTOP_PACKAGES_UNINSTALL -11. Install branding module_desktop_branding $de -12. Install Armbian Imager module_appimage install app=armbian-imager -13. Add user to groups sudo netdev audio video dialout plugdev input bluetooth systemd-journal ssh -14. Profile sync daemon (psd) touch ~/.activate_psd, sudoers entry -15. Sync skel to existing users module_update_skel install -16. Start display manager skipped if _desktop_in_container -17. Enable auto-login module_desktops auto de=$de +1. Validate args de= and tier= both required; tier must be minimal|mid|full +2. Resolve target user module_desktop_getuser +3. Parse YAML at target tier module_desktop_yamlparse $de $arch $release $tier +4. Validate package list exit if DESKTOP_PACKAGES / DESKTOP_PRIMARY_PKG empty +5. Warn on unsupported DESKTOP_SUPPORTED != yes → stderr warning, continue +6. Suppress encfs prompt debconf-set-selections +7. Configure custom repo module_desktop_repo $de (no-op if no repo: block) +8. apt update pkg_update +9. Reset ACTUALLY_INSTALLED array used by pkg_install to record new packages +10. apt install desktop pkgs pkg_install $DESKTOP_PACKAGES ← bail on failure +11. apt install + register DM pkg_install $DESKTOP_DM ← bail on failure + /etc/X11/default-display-manager +12. (Armbian) install plymouth if /etc/apt/sources.list.d/armbian.{list,sources} present +13. Save install manifest /etc/armbian/desktop/.packages and .tier +14. Purge unwanted packages apt-get remove --purge $DESKTOP_PACKAGES_UNINSTALL +15. Install branding module_desktop_branding $de +16. Add user to groups sudo netdev audio video dialout plugdev input bluetooth systemd-journal ssh +17. Profile sync daemon (psd) touch ~/.activate_psd, sudoers entry +18. Sync skel to existing users module_update_skel install (with chown -R safety net) +19. Stop other DMs gdm3/lightdm/sddm one by one +20. Start display manager systemctl start display-manager ← container path skips this +21. Switch default.target systemctl set-default graphical.target ONLY if step 20 succeeded +22. Enable auto-login module_desktops auto de=$de ``` -The remove pipeline (`module_desktops remove`) reverses the user-visible parts: disables auto-login, stops the display manager, purges the DM and primary package (`apt`'s autoremove handles dependencies), and removes the Armbian Imager AppImage. It does **not** uninstall the full `DESKTOP_PACKAGES` set or undo branding — both are deliberate to avoid removing things the user may now depend on. +If step 10 or 11 fails, the function returns 1 with no further state changes — the manifest is not written, `default.target` stays at `multi-user`, no DM is started. The system is in the same state as if the install had never run. + +## Lifecycle: remove + +```text +1. Validate args de= required +2. Read installed tier marker /etc/armbian/desktop/.tier (default: minimal) +3. Parse YAML at the installed module_desktop_yamlparse $de $arch $release $installed_tier + tier +4. Disable auto-login module_desktops manual de=$de +5. Stop display manager systemctl stop display-manager +6. Switch default.target systemctl set-default multi-user.target +7. Isolate to multi-user systemctl isolate multi-user.target (drops running session + to console immediately, no reboot needed) +8. Compute removable set from /etc/armbian/desktop/.packages + fallback: walk DESKTOP_PACKAGES through dpkg-query +9. pkg_remove apt-get autopurge +10. Delete manifest files rm /etc/armbian/desktop/.{packages,tier} +11. pkg_clean apt-get clean — reclaim downloaded .deb cache +``` + +The `set-default` and `isolate` calls together ensure the user gets a console login on tty1 immediately after the uninstall, without needing to reboot. Without them, the system stays pinned to `graphical.target` with no DM behind it and the local console is blank. + +## Lifecycle: upgrade and downgrade + +`upgrade` and `downgrade` are the two halves of `_module_desktops_change_tier`: + +```text +1. Validate args de= and tier= required; tier must be minimal|mid|full +2. Read current tier marker /etc/armbian/desktop/.tier (must exist) +3. Validate direction upgrade refuses target <= current + downgrade refuses target >= current + same tier → no-op message, exit 0 +4. Parse YAML twice once at current tier, once at target tier + store the package lists in two bash arrays +5. Compute set difference (awk on per-line printf input) + upgrade: to_install = target - current + downgrade: removable = current - target +6. (downgrade only) intersect removable ∩ .packages — never touch + packages the user installed manually + outside the desktop install path +7. Apply pkg_install (upgrade) or pkg_remove (downgrade) +8. Update manifest append new packages, or remove drained ones +9. Update tier marker /etc/armbian/desktop/.tier +``` + +`set-tier` is a thin front-end over the same helper that auto-detects direction from the current tier vs the target. It's the entry point used by the dialog menu's "Change to " buttons. ## Container and CI awareness @@ -348,28 +637,35 @@ The remove pipeline (`module_desktops remove`) reverses the user-visible parts: Inside a container the install pipeline still does packages, branding, and skel work, but **skips**: - Stopping or starting any display manager +- The `set-default graphical.target` switch - Restarting the display manager after auto-login changes +- The `systemctl isolate` call on remove - Running per-desktop `postinst/.sh` hooks This makes the same code path usable for image preseeding inside Docker without needing parallel "container mode" branches. ## Adding a new desktop -1. **Create the YAML.** Drop a new file at `tools/modules/desktops/yaml/.yaml` following the [schema](#yaml-schema). Minimum required fields: `display_manager`, `status`, `packages`, and at least one entry under `releases.` with an `architectures` list. -2. **(Optional) Custom repo.** Add a `repo:` block if the DE is not in the distro's default repositories. Pin the keyring path under `/usr/share/keyrings/`. -3. **(Optional) Postinst hook.** Drop `tools/modules/desktops/postinst/.sh` for any per-DE configuration that has to run after `apt install`. Container/CI runs are skipped automatically. -4. **(Optional) Branding overrides.** Branding lives in shared directories, so most desktops do not need any per-DE assets — only add files when the DE needs something different. -5. **Smoke test the parser:** +1. **Create the YAML.** Drop a new file at `tools/modules/desktops/yaml/.yaml` following the [schema](#yaml-schema). Minimum required fields: `display_manager`, `status`, `tiers.minimal.packages`, and at least one entry under `releases.` with an `architectures` list. +2. **(Optional) Per-DE tier overrides.** Add a `tiers.mid` and/or `tiers.full` block only if you need to override common defaults. Most DEs inherit common's mid/full unchanged. +3. **(Optional) `tier_overrides`.** Add per-arch or per-release-per-arch removals only when there's a known package availability hole specific to this DE. Cross-DE holes belong in `common.yaml`. +4. **(Optional) Custom repo.** Add a `repo:` block if the DE is not in the distro's default repositories. Pin the keyring path under `/usr/share/keyrings/`. +5. **(Optional) Postinst hook.** Drop `tools/modules/desktops/postinst/.sh` for any per-DE configuration that has to run after `apt install`. Container/CI runs are skipped automatically. +6. **(Optional) Branding overrides.** Branding lives in shared directories, so most desktops do not need any per-DE assets — only add files when the DE needs something different. +7. **Smoke test the parser at every tier:** ```bash cd configng - python3 tools/modules/desktops/scripts/parse_desktop_yaml.py \ - tools/modules/desktops/yaml trixie arm64 + for tier in minimal mid full; do + python3 tools/modules/desktops/scripts/parse_desktop_yaml.py \ + tools/modules/desktops/yaml trixie arm64 --tier $tier + echo "---" + done ``` - All `DESKTOP_*` variables should print, and `DESKTOP_SUPPORTED="yes"` for any (release, arch) pair you listed in the YAML. + All `DESKTOP_*` variables should print, `DESKTOP_SUPPORTED="yes"` for any (release, arch) pair you listed in the YAML, and `DESKTOP_TIER` should match the requested tier. -6. **List-mode sanity check:** +8. **List-mode sanity check:** ```bash python3 tools/modules/desktops/scripts/parse_desktop_yaml.py \ @@ -378,7 +674,64 @@ This makes the same code path usable for image preseeding inside Docker without Your new desktop should appear in the TSV output for the (release, arch) combinations you declared. -7. **End-to-end test** in a disposable VM or container with `armbian-config desktop install de=`. +9. **End-to-end test** in a disposable VM or container: + + ```bash + armbian-config --api module_desktops install de= tier=minimal + armbian-config --api module_desktops upgrade de= tier=mid + armbian-config --api module_desktops upgrade de= tier=full + armbian-config --api module_desktops downgrade de= tier=minimal + armbian-config --api module_desktops remove de= + ``` + +10. **Add menu entries** in `tools/json/config.system.json` if you want the DE to appear in the dialog menu. Existing desktops use this slot allocation per DE: + + | ID slot | Action | + |---|---| + | `*01` | install minimal | + | `*02` | uninstall | + | `*03` | enable autologin | + | `*04` | disable autologin | + | `*05` | install mid | + | `*06` | install full | + | `*07` | change to minimal | + | `*08` | change to mid | + | `*09` | change to full | + + The `*07-*09` change-tier entries use `module_desktops set-tier` and gate visibility with `module_desktops status de= && ! module_desktops at-tier de= tier=`. + +## Common pitfalls + +### `packages_uninstall` cascade + +Listing a package in `tiers.minimal.packages_uninstall` runs `apt-get remove --purge` on it after the install. If that package is a hard `Depends:` of any meta package the DE install pulled in, apt's autoremove cascade will yank the meta package along with it — and on systems with `APT::Get::AutomaticRemove "true"` (Ubuntu noble/plucky), the cascade keeps going and rips out a chunk of the desktop. Real examples that bit us: + +- Listing any `xfce4-goodies` plugin (e.g. `xfce4-clipman-plugin`) yanks `xfce4-goodies` itself, then half the desktop. +- Listing `language-selector-gnome` yanks `gnome-control-center` (which has it as a hard Depends on Ubuntu), so the user loses Settings. +- Listing `kdeconnect` or `khelpcenter` yanks `neon-desktop`. + +**Rule**: never put a `Depends:` of a metapackage you ship into `packages_uninstall`. Verify with `apt-cache rdepends --installed ` before adding anything. + +### Gnome `daemon.conf` vs `custom.conf` + +Both Debian and Ubuntu ship a `gdm3` package, but they read different config files: + +- Debian (any release): `/etc/gdm3/daemon.conf` +- Ubuntu (any release): `/etc/gdm3/custom.conf` + +`module_desktops auto` branches on `ID=ubuntu` from `/etc/os-release`, **not** on the release codename. Earlier versions of the code branched on codename and wrote to the wrong file on Debian bookworm. + +The `auto` path also edits the file in place via sed (preserving any user customization like `WaylandEnable=false`) rather than overwriting it with a fresh `cat > $file`. + +### `login` regex anchoring + +The stock Ubuntu noble `/etc/gdm3/custom.conf` template ships with a commented sample line: + +``` +# AutomaticLoginEnable = true +``` + +An unanchored `grep` for `AutomaticLoginEnable\s*=\s*true` matches this comment, and `module_desktops login` returns 0 (autologin enabled) on every fresh install where the user has never touched autologin. The fix is `^AutomaticLoginEnable[[:space:]]*=[[:space:]]*true` — anchored at line start so the comment doesn't match. ## Security notes From f9fb5942136398a6f1d1ccee252474508244ee69 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Sat, 11 Apr 2026 22:34:39 +0200 Subject: [PATCH 03/13] desktops: clean up headers for the mkdocs sidebar TOC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The auto-generated left-nav was rendering badly: - YAML schema sub-headers like 'Per-release block (releases.)' had angle brackets that mkdocs's TOC generator passed through as raw text, producing 'releases ' with a literal '<' / '>' character and a missing dot. - Bash module API headers carried the full function signature inline, e.g. module_desktops [de=] [tier=] [arch=] [release=] This wrapped over multiple lines in the sidebar and looked like garbage. Two fixes: 1. Drop angle-bracket parameter hints from every header. The schema sections become 'Tier blocks', 'Per-release block', 'Custom repository block', etc. The information that used to be in the header (the parent path like releases.) is already covered in the body text. 2. For each bash module function, the header is now just the function name (no signature, no inline backticks). The signature moves into a fenced code block on the line below — visible to readers, invisible to the sidebar TOC. Before: ### `module_desktops [de=] [tier=] [arch=] [release=]` After: ### module_desktops ```text module_desktops [de=] [tier=] [arch=] [release=] ``` Same treatment for the Common pitfalls subheaders, the parser helper section header, and the common.yaml / 'List and JSON list modes' subheaders — drop the inline backticks so the sidebar formatting is consistent. No content changes; this is purely a sidebar-rendering fix. --- docs/Developer-Guide_Desktops.md | 72 ++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/docs/Developer-Guide_Desktops.md b/docs/Developer-Guide_Desktops.md index 54592c7e..8831dba1 100644 --- a/docs/Developer-Guide_Desktops.md +++ b/docs/Developer-Guide_Desktops.md @@ -113,7 +113,7 @@ Each desktop is defined in a single YAML file under `tools/modules/desktops/yaml | `releases` | mapping | yes | Per-release overrides keyed by release codename (`bookworm`, `trixie`, `noble`, `plucky`, ...). | | `repo` | mapping | optional | Custom APT repository, see below. | -### Tier blocks (`tiers.`) +### Tier blocks | Field | Type | Description | |---|---|---| @@ -123,7 +123,7 @@ Each desktop is defined in a single YAML file under `tools/modules/desktops/yaml The first DE-specific package that survives all filters becomes `DESKTOP_PRIMARY_PKG`, used by `module_desktops status` for `dpkg -l` checks. It must come from the DE's own `tiers.minimal.packages` block, not from `common.yaml`, otherwise every DE would share the same primary package. -### Per-release block (`releases.`) +### Per-release block The release block is **orthogonal** to the tier walk: it applies to whatever tier is being installed. Use it for things that vary by release rather than by user choice (e.g. trixie's pulseaudio→pipewire swap, bookworm's `gnome-calculator` addition). @@ -155,7 +155,7 @@ Use the per-arch layer for permanent arch-wide holes (e.g. `blender` always miss `tier_overrides` can live in `common.yaml` (applies to every DE) or in a per-DE YAML (applies only to that DE). The parser merges common first, then per-DE. -### Custom repository block (`repo`) +### Custom repository block | Field | Type | Description | |---|---|---| @@ -253,7 +253,7 @@ releases: architectures: [arm64, amd64] ``` -### `common.yaml` +### common.yaml `common.yaml` carries the per-tier defaults that apply to every desktop, the browser substitution table, and any cross-DE `tier_overrides`. Per-DE YAMLs only declare a `tiers` block when they want to add packages on top of common or override common-tier entries. @@ -359,7 +359,7 @@ The per-release layer is needed because the same arch can resolve differently ac - `chromium` isn't built for riscv64 in either Debian or Ubuntu. - `firefox` isn't built for noble/plucky riscv64 either. -## Python helper: `parse_desktop_yaml.py` +## Python helper: parse_desktop_yaml.py Single-purpose CLI that bash modules invoke via `python3`. All YAML parsing and validation happens here so the bash side stays free of YAML logic. @@ -421,7 +421,7 @@ The parser is strict about top-level structure but tolerant of malformed sub-nod - **Path traversal guard** — `de_name` is resolved against `yaml_dir` via `os.path.realpath`/`commonpath`. Anything outside the directory (`../...`, absolute paths, symlink escapes) is rejected with `Error: invalid desktop name ''` and exit 1. - **Tolerant normalization** — `tiers`, `releases`, `architectures`, `tier_overrides`, `repo`, every list field passes through `_as_dict` / `_as_list` helpers. Wrong-typed nodes coerce to safe empty defaults (`{}` or `[]`) instead of raising `AttributeError` or doing surprising substring matches like `arch in "arm64"`. -### `--list` / `--list-json` mode +### List and JSON list modes Iterates every `*.yaml` (excluding `common.yaml`), parses each one's release block, and prints **only entries supported on the requested (release, arch)**. Used by `module_desktops install` to show available desktops on error and by `module_desktops supported` to expose a machine-readable catalog. These modes do not require `--tier`. @@ -429,7 +429,11 @@ Iterates every `*.yaml` (excluding `common.yaml`), parses each one's release blo All functions are loaded by configng's module loader. They share global state (`DESKTOP_*` variables, `desktops_dir`, `DISTROID`) — call sites must follow the documented order. -### `module_desktops [de=] [tier=] [arch=] [release=]` +### module_desktops + +```text +module_desktops [de=] [tier=] [arch=] [release=] +``` Top-level dispatcher. The `de=`, `tier=`, `arch=`, `release=` arguments are parsed positionally from `$@`. @@ -469,7 +473,11 @@ Two files per installed desktop, both under `/etc/armbian/desktop/`: | `sddm` | `/etc/sddm.conf.d/autologin.conf` (drop-in, non-destructive) | | `lightdm` | `/etc/lightdm/lightdm.conf.d/22-armbian-autologin.conf` (drop-in, non-destructive) | -### `module_desktop_yamlparse [arch] [release] [tier]` +### module_desktop_yamlparse + +```text +module_desktop_yamlparse [arch] [release] [tier] +``` Wraps `parse_desktop_yaml.py`. Resets all `DESKTOP_*` globals, runs the helper, and `eval`s its stdout. Returns 1 on parse failure (with the parser's stderr surfaced). @@ -487,15 +495,27 @@ echo "$DESKTOP_TIER" # → full echo "$DESKTOP_PACKAGES" # → minimal + mid + full set, with browser resolved ``` -### `module_desktop_yamlparse_list [arch] [release]` +### module_desktop_yamlparse_list + +```text +module_desktop_yamlparse_list [arch] [release] +``` Calls the parser with `--list` and prints TSV to stdout. Used to assemble the "Available: ..." hint shown when `install` is invoked without `de=`. -### `module_desktop_supported [arch] [release]` +### module_desktop_supported + +```text +module_desktop_supported [arch] [release] +``` Convenience wrapper around `module_desktop_yamlparse` that returns 0/1 based on `DESKTOP_SUPPORTED`. Suppresses parser stderr — meant for predicates and CI gates. -### `module_desktop_repo ` +### module_desktop_repo + +```text +module_desktop_repo +``` Sets up a custom APT source. Must be called **after** `module_desktop_yamlparse` because it consumes `DESKTOP_REPO_URL`, `DESKTOP_REPO_KEY_URL`, `DESKTOP_REPO_KEYRING`. @@ -508,7 +528,11 @@ Behavior: A no-op if the YAML has no `repo:` block. -### `module_desktop_branding ` +### module_desktop_branding + +```text +module_desktop_branding +``` Copies branding assets and runs the optional postinst hook. Idempotent — every step is guarded with `[[ -d ... ]]`. @@ -526,11 +550,19 @@ Copies branding assets and runs the optional postinst hook. Idempotent — every The distributor logo for GNOME Settings → About / KDE Info Center / etc. is **not** installed from here — that file ships from `armbian-base-files` so it stays in sync with the `LOGO=` line in `/etc/os-release`. -### `module_desktop_getuser` +### module_desktop_getuser + +```text +module_desktop_getuser +``` Returns the first non-root, non-system user with a real login shell. Prefers `$SUDO_USER` if set and not root, otherwise scans `/etc/passwd` for the first entry with `1000 ≤ uid < 65534` and a shell that does not match `nologin|false`. Exits 1 if none is found. -### `module_update_skel install` +### module_update_skel + +```text +module_update_skel install +``` Walks `getent passwd`, and for every regular user (`1000 ≤ uid < 65534`, home directory exists, not root): @@ -541,7 +573,11 @@ Walks `getent passwd`, and for every regular user (`1000 ≤ uid < 65534`, home The recursive `chown` is critical: other package postinst scripts (caja, nemo, gnome-keyring, …) routinely leak root-owned files into the user's `~/.config` directory on first install. Without the recursive chown, those tools refuse to start on first login because they can't write their own config dirs. -### `module_appimage app=` +### module_appimage + +```text +module_appimage app= +``` Standalone AppImage helper. The internal `APPIMAGE_REPO` registry maps logical app names (e.g. `armbian-imager`) to GitHub `owner/repo` slugs and downloads the appropriate architecture-suffixed AppImage from the latest release. `module_appimage install` also installs `libfuse2`, `fuse3`, and the `libgles2`/`libegl1`/`libgl1`/`libgl1-mesa-dri` runtime so the AppImage can launch. @@ -702,7 +738,7 @@ This makes the same code path usable for image preseeding inside Docker without ## Common pitfalls -### `packages_uninstall` cascade +### packages_uninstall cascade Listing a package in `tiers.minimal.packages_uninstall` runs `apt-get remove --purge` on it after the install. If that package is a hard `Depends:` of any meta package the DE install pulled in, apt's autoremove cascade will yank the meta package along with it — and on systems with `APT::Get::AutomaticRemove "true"` (Ubuntu noble/plucky), the cascade keeps going and rips out a chunk of the desktop. Real examples that bit us: @@ -712,7 +748,7 @@ Listing a package in `tiers.minimal.packages_uninstall` runs `apt-get remove --p **Rule**: never put a `Depends:` of a metapackage you ship into `packages_uninstall`. Verify with `apt-cache rdepends --installed ` before adding anything. -### Gnome `daemon.conf` vs `custom.conf` +### Gnome daemon.conf vs custom.conf Both Debian and Ubuntu ship a `gdm3` package, but they read different config files: @@ -723,7 +759,7 @@ Both Debian and Ubuntu ship a `gdm3` package, but they read different config fil The `auto` path also edits the file in place via sed (preserving any user customization like `WaylandEnable=false`) rather than overwriting it with a fresh `cat > $file`. -### `login` regex anchoring +### login regex anchoring The stock Ubuntu noble `/etc/gdm3/custom.conf` template ships with a commented sample line: From b3376b63953513c6232eef61d53d2e8d37756ab7 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Sat, 11 Apr 2026 22:43:37 +0200 Subject: [PATCH 04/13] css: keep schema-table identifiers from wrapping mid-word MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tables in the developer guides use the first column for identifiers (field names like 'display_manager', package names like 'gnome-control-center'). When the column was narrow, mkdocs material was wrapping the identifier between letters — 'display_manage' / 'r' — which was unreadable. Add a CSS rule that says: in any table, if the first column's cell contains a element, don't wrap that code. The rule is scoped to in the first cell so prose tables (where the first column might be a sentence) are unaffected. The result is that schema tables auto-size their first column to fit the longest identifier without wrapping mid-word, while keeping the rest of the columns flexible. --- docs/css/armbian-extra.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/css/armbian-extra.css b/docs/css/armbian-extra.css index f4f11b48..9c4bb2b7 100644 --- a/docs/css/armbian-extra.css +++ b/docs/css/armbian-extra.css @@ -6,6 +6,17 @@ display: table; } +/* Prevent the first column of schema/reference tables from wrapping + * mid-identifier. Identifiers like 'display_manager' or 'gnome-control-center' + * are common in the developer-guide tables and look terrible when broken + * across lines (e.g. 'display_manage' / 'r'). + * Scope to in the first cell so prose tables are unaffected. + */ +.md-typeset table:not([class]) td:first-child code, +.md-typeset table:not([class]) th:first-child code { + white-space: nowrap; +} + /* Style for only specially tagged bash blocks */ pre.custom-bash-block, code.custom-bash-block { font-size: 24pt; From 2b50f2016439dd73eb97c9ba6ada69d181f94574 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Sat, 11 Apr 2026 22:56:18 +0200 Subject: [PATCH 05/13] desktops: drop line numbers from the lifecycle code blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mkdocs config enables linenums globally for every fenced code block via pymdownx.highlight, which doubled up with the inline '1. 2. 3.' step prefixes inside the install / remove / upgrade-downgrade lifecycle blocks. The result was a left gutter of 1..N alongside the inline 1. 2. 3. on every line — two parallel numbering systems competing for attention. Override per-block with `{ .text linenums="0" }` on the three lifecycle code fences. The inline step numbers are the canonical step IDs (referenced from the prose around them); the auto gutter was redundant. Other code blocks in the doc keep the global gutter — only the lifecycle ascii-art blocks change. --- docs/Developer-Guide_Desktops.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Developer-Guide_Desktops.md b/docs/Developer-Guide_Desktops.md index 8831dba1..f9e711a0 100644 --- a/docs/Developer-Guide_Desktops.md +++ b/docs/Developer-Guide_Desktops.md @@ -587,7 +587,7 @@ Not called from the desktop install path by default. The `armbian-imager` AppIma The install pipeline in `module_desktops install` is intentionally linear and idempotent-friendly. **Every step that touches system state is gated on the previous step's success.** -```text +```{ .text linenums="0" } 1. Validate args de= and tier= both required; tier must be minimal|mid|full 2. Resolve target user module_desktop_getuser 3. Parse YAML at target tier module_desktop_yamlparse $de $arch $release $tier @@ -617,7 +617,7 @@ If step 10 or 11 fails, the function returns 1 with no further state changes — ## Lifecycle: remove -```text +```{ .text linenums="0" } 1. Validate args de= required 2. Read installed tier marker /etc/armbian/desktop/.tier (default: minimal) 3. Parse YAML at the installed module_desktop_yamlparse $de $arch $release $installed_tier @@ -640,7 +640,7 @@ The `set-default` and `isolate` calls together ensure the user gets a console lo `upgrade` and `downgrade` are the two halves of `_module_desktops_change_tier`: -```text +```{ .text linenums="0" } 1. Validate args de= and tier= required; tier must be minimal|mid|full 2. Read current tier marker /etc/armbian/desktop/.tier (must exist) 3. Validate direction upgrade refuses target <= current From 185b2ec8ca721235112f952a58707aa26766cf47 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Sun, 12 Apr 2026 00:34:54 +0200 Subject: [PATCH 06/13] desktops: fix broken #tier-overrides anchor link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Top-level fields table at line 112 links to '#tier-overrides', but the heading 'tier_overrides' slugifies to 'tier_overrides' under Python-Markdown's default slugifier (underscores are preserved as part of the word-character class). The link silently 404s when clicked. Add an explicit anchor: `### tier_overrides {#tier-overrides}`. This is the same pattern already used for the Lifecycle: install heading further down the file. Verified the other internal fragment link in the same table — `#tier-blocks` for the heading `### Tier blocks` — slugifies correctly to 'tier-blocks' (whitespace becomes the separator), so it does NOT need an explicit anchor and is left alone. The other two internal fragment links in the file (`#yaml-schema` from the 'Adding a new desktop' walkthrough and `#lifecycle-install` from the bash module API table) both resolve correctly already. --- docs/Developer-Guide_Desktops.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Developer-Guide_Desktops.md b/docs/Developer-Guide_Desktops.md index f9e711a0..ab16da60 100644 --- a/docs/Developer-Guide_Desktops.md +++ b/docs/Developer-Guide_Desktops.md @@ -134,7 +134,7 @@ The release block is **orthogonal** to the tier walk: it applies to whatever tie | `packages_remove` | list | Packages filtered out of the merged install list. | | `packages_uninstall` | list | Packages purged after install on this release only. | -### tier_overrides +### tier_overrides {#tier-overrides} `tier_overrides` is for **package availability holes**: a tier package that exists on most arches/releases but is missing on one specific combination. The schema has two layers: From 9b2f734862fd4f82eaed8335802a3dfe9383c3d0 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Sun, 12 Apr 2026 20:42:06 +0200 Subject: [PATCH 07/13] desktops: document the matrix audit automation Adds a "Matrix audit automation" section to the desktops developer guide describing the weekly GitHub Actions workflow that audits the YAML matrix and opens bot PRs for drift. Covers: - the two drift classes (missing releases, package holes) and why the deterministic scanner + LLM applier split exists - audit.py report shape, armbian/build dependency, and flags - audit_prompt.py constraints pinned into the Claude prompt - maintenance-desktop-audit.yml triggers, concurrency, each step, the claude-code-action tool allow-list required for edits to actually land, and the execution-log artifact used for diagnosing zero-edit runs - permissions block - a maintainer review checklist for incoming draft bot PRs --- docs/Developer-Guide_Desktops.md | 105 +++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/docs/Developer-Guide_Desktops.md b/docs/Developer-Guide_Desktops.md index ab16da60..b4e74aef 100644 --- a/docs/Developer-Guide_Desktops.md +++ b/docs/Developer-Guide_Desktops.md @@ -736,6 +736,111 @@ This makes the same code path usable for image preseeding inside Docker without The `*07-*09` change-tier entries use `module_desktops set-tier` and gate visibility with `module_desktops status de= && ! module_desktops at-tier de= tier=`. +## Matrix audit automation + +The desktop matrix covers several DEs × several releases × several architectures, and two kinds of drift tend to accumulate silently: + +1. **Missing releases** — `armbian/build` adds a new release to `config/distributions/` (e.g. Ubuntu `resolute`) but no DE YAML grows a release block for it, so the desktop can't be installed on that release at all. +2. **Package holes** — an entry in the resolved `DESKTOP_PACKAGES` set is no longer published for some `(release, arch)` pair (archive removed it, or it was never built for that arch), so `apt` fails at install time with `E: Unable to locate package`. + +A weekly GitHub Actions workflow detects both, hands the findings to Claude Code to propose YAML edits, and opens a draft PR for a maintainer to review. + +### Components + +```text +tools/modules/desktops/github/ +├── audit.py # deterministic scanner — emits audit-report.json +├── audit_prompt.py # renders the report into a Claude prompt +└── audit_apply.py # legacy direct-API applier (unused by the workflow) + +.github/workflows/ +└── maintenance-desktop-audit.yml # the scheduled workflow +``` + +Only the scanner talks to the network; the LLM never fetches package metadata itself. That keeps the "what is broken" signal reproducible and cache-friendly, and confines all non-determinism to the "how should we fix it" step. + +### `audit.py` + +Walks `tools/modules/desktops/yaml/` against: + +- `armbian/build`'s `config/distributions/.conf` (loaded from a sibling checkout passed via `--build-repo`) to get the set of releases and their support statuses (`supported`, `csc`, `eos`, …). Anything `eos` is skipped. +- `packages.debian.org` and `packages.ubuntu.com` — one `urllib` request per `(release, arch, package)` tuple, parallelised with `ThreadPoolExecutor`. Responses are cached in-process for the run. + +Report shape (`audit-report.json`): + +```json +{ + "scanned_releases": ["bookworm", "noble", "plucky", "trixie"], + "build_distributions": { "": { "name": "...", "support": "supported|csc|eos", "architectures": [...] } }, + "missing_releases": [ { "release": "resolute", "support_status": "csc", "architectures": [...] } ], + "package_holes": [ { "de": "xfce", "release": "trixie", "arch": "riscv64", "tier": "full", "missing": ["libfoo"] } ], + "skipped_desktops": ["bianbu", "budgie", "deepin", "kde-neon"], + "stats": { "desktops": 8, "scope": 4, "holes": 0, "package_lookups": 0 } +} +``` + +Desktops with `status: unsupported` in their YAML are listed in `skipped_desktops` and not audited — drift in an unsupported DE isn't actionable. + +Flags: `--tier {minimal,mid,full}` narrows the scope; `--release ` audits a single release; `--skip-network` is a dry-run that only reports `missing_releases`. + +### `audit_prompt.py` + +Renders the JSON report into a single text prompt (no markdown-in-markdown gymnastics; the report JSON is embedded in fenced blocks). The prompt pins Claude to: + +- touch only YAML files under `tools/modules/desktops/yaml/` +- address **every** finding, not just the first +- prefer edits to `common.yaml`'s `tier_overrides` block for package holes (one place, applies to every DE) over duplicating `packages_remove` entries in per-DE YAMLs +- for missing releases, add a release block to each `status: supported` DE YAML, copying the shape from an existing block and adjusting per-release deltas only where needed +- always add an inline comment explaining **why** a hole exists, so future readers can distinguish a transient archive gap from a permanent upstream-port limitation +- preserve the existing 2-space indentation +- if the report is empty, say so and make no edits + +### `maintenance-desktop-audit.yml` + +Triggers: + +- `schedule: '0 6 * * 1'` — Mondays 06:00 UTC. Release and package availability change slowly, so weekly is enough and cheap. +- `workflow_dispatch` — with optional `tier`, `release`, and `dry_run` inputs. `dry_run: true` stops after the deterministic audit and attaches `audit-report.json` without calling Claude or opening a PR. + +Concurrency: `group: desktop-audit`, `cancel-in-progress: false` — two scheduled runs will never race, and a manual dispatch queues behind the scheduled run rather than killing it. + +Job steps, in order: + +1. **Checkout configng** at the workspace root (no `path:`) so `claude-code-action` finds `.git`. +2. **Checkout `armbian/build`** into `armbian-build/` with `fetch-depth: 1` — the audit only reads `config/distributions/`, so shallow is fine. +3. **Set up Python 3.12** and `pip install pyyaml`. +4. **Run `audit.py`** — writes `audit-report.json`, appends a markdown summary table to `$GITHUB_STEP_SUMMARY`, and sets `steps.audit.outputs.actionable` to `true` iff `missing_releases` or `package_holes` is non-empty. +5. **Prepare Claude prompt** (`audit_prompt.py`) — only if `actionable` and not a dry run. +6. **Upload `audit-report`** artifact (always, 30-day retention) — useful even on zero-hole runs as historical record. +7. **`anthropics/claude-code-action@v1`** with: + - `claude_code_oauth_token: secrets.CLAUDE_CODE_OAUTH_TOKEN` (Max subscription token — no per-run API charges). + - `claude_args: --max-turns 30 --permission-mode acceptEdits --allowed-tools Edit,Write,Read,Glob,Grep,Bash(git:*)`. `acceptEdits` plus the explicit allow-list is required: without them the action's default tool gate denies Edit/Write and the branch stays empty. `Bash(git:*)` only permits read-only git inspection; no shell execution surface. +8. **Stash Claude execution log** — copies `${RUNNER_TEMP}/claude-execution-output.json` into the workspace; uploaded as the `claude-execution-output` artifact with `if: always()` so a failed or zero-edit run is debuggable from the transcript without a re-run. +9. **Clean up temp files** — removes `armbian-build/`, `audit-report.json`, `claude-prompt.txt`, and `claude-execution-output.json` from the working tree so `peter-evans/create-pull-request` sees only Claude's YAML edits. +10. **`peter-evans/create-pull-request@v6`** — branch `bot/desktop-matrix-audit`, base `main`, `add-paths: tools/modules/desktops/yaml/*`, `delete-branch: true`, `draft: true`, labels `bot`, `desktops`, `documentation`. PR body is `steps.claude.outputs.structured_output` (Claude's own summary of what it changed and why). If Claude produced no diff, the branch is not ahead of main and no PR is opened — the workflow finishes green with only the audit artifact. + +### Permissions + +```yaml +permissions: + contents: write # push to bot/desktop-matrix-audit + pull-requests: write # open the PR + id-token: write # claude-code-action OIDC +``` + +### Reviewing a bot PR + +Bot PRs open as **draft** on purpose. A human check before merge: + +1. Read Claude's PR body — it should list every file it changed and the reason. +2. Confirm the diff is scoped to `tools/modules/desktops/yaml/`. Any out-of-scope file is a red flag (the workflow's `add-paths` should already prevent this, but verify). +3. For each missing-release addition: spot-check that the new release block is a sensible copy of an existing one (e.g. a `resolute` block for `xfce.yaml` should look like the `trixie` or `noble` block, not a half-written stub). +4. For each package-hole edit: confirm it lives in `common.yaml`'s `tier_overrides` where it belongs, not duplicated per-DE. +5. For each WHY comment: confirm it's accurate. "not yet in trixie" ages out; "no upstream riscv64 port" doesn't. +6. Mark ready for review and merge normally. `delete-branch: true` cleans up on merge. + +If Claude judged the report non-actionable (e.g. the only finding is a `csc`-tier release a maintainer wants to hold off on), the run ends with the `audit-report` artifact present and no PR — inspect the artifact and the `claude-execution-output` log to confirm. + ## Common pitfalls ### packages_uninstall cascade From 76ee6de07bff78ace9e23e534007d5347f171782 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Sun, 12 Apr 2026 21:21:02 +0200 Subject: [PATCH 08/13] desktops: document optional repo.preferences field Adds a row for `preferences` to the custom-repo schema table plus an explainer on priority thresholds, the generated stanza shape, and the parse_desktop_yaml.py validation behavior. Matches the configng change that introduces the field. --- docs/Developer-Guide_Desktops.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/Developer-Guide_Desktops.md b/docs/Developer-Guide_Desktops.md index b4e74aef..7c336c0c 100644 --- a/docs/Developer-Guide_Desktops.md +++ b/docs/Developer-Guide_Desktops.md @@ -162,6 +162,17 @@ Use the per-arch layer for permanent arch-wide holes (e.g. `blender` always miss | `url` | string | Base URL for `deb [signed-by=...] main`. | | `key_url` | string | URL to the GPG key (ASCII-armored). | | `keyring` | string | Path to the dearmored keyring file, e.g. `/usr/share/keyrings/neon.gpg`. | +| `preferences` | list (optional) | APT pin preferences written to `/etc/apt/preferences.d/`. Each entry needs `origin`, `suite`, and `priority` (integer). Removed on uninstall. | + +`preferences` is rarely needed — only when a vendor archive must outrank the distro for a given `(origin, suite)` pair. Each list entry becomes one stanza: + +``` +Package: * +Pin: release o=, n= +Pin-Priority: +``` + +Priorities above 1000 let apt downgrade a package from the distro to the pinned archive's version; below 1000 only allows upgrades. Entries missing any required field are skipped with a warning from `parse_desktop_yaml.py`. ### Example From 3d4133d986ec751723b5a72528e5a426eb0d8d98 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Sun, 12 Apr 2026 21:51:21 +0200 Subject: [PATCH 09/13] desktops: document repo.suite and repo.components fields Schema table gains rows for the two optional fields introduced by configng, with a bianbu-flavored explainer for when they apply (frozen per-release snapshots, non-default component set). --- docs/Developer-Guide_Desktops.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/Developer-Guide_Desktops.md b/docs/Developer-Guide_Desktops.md index 7c336c0c..0213584c 100644 --- a/docs/Developer-Guide_Desktops.md +++ b/docs/Developer-Guide_Desktops.md @@ -159,10 +159,14 @@ Use the per-arch layer for permanent arch-wide holes (e.g. `blender` always miss | Field | Type | Description | |---|---|---| -| `url` | string | Base URL for `deb [signed-by=...] main`. | +| `url` | string | Base URL for `deb [signed-by=...] `. | | `key_url` | string | URL to the GPG key (ASCII-armored). | | `keyring` | string | Path to the dearmored keyring file, e.g. `/usr/share/keyrings/neon.gpg`. | -| `preferences` | list (optional) | APT pin preferences written to `/etc/apt/preferences.d/`. Each entry needs `origin`, `suite`, and `priority` (integer). Removed on uninstall. | +| `suite` | string (optional) | Suite path that follows the URL in the source line. Defaults to the release codename (e.g. `noble`). Regex-validated to `^[A-Za-z0-9._/-]+$`. Per-release override: `releases..repo_suite`. | +| `components` | list (optional) | Components that follow the suite. Defaults to `[main]`. Each entry regex-validated to `^[A-Za-z0-9._-]+$`; invalid entries are dropped with a warning. Per-release override: `releases..repo_components`. | +| `preferences` | list (optional) | APT pin preferences written to `/etc/apt/preferences.d/`. Each entry needs `origin`, `suite`, and `priority` (positive integer). Removed on uninstall. | + +`suite` and `components` exist for vendor archives whose layout doesn't match the default ` main` convention. For example, SpacemiT's K1 RISC-V archive pins a frozen snapshot per Ubuntu release (`noble/snapshots/v2.2`, `resolute/snapshots/v3.0`) and mirrors all four Ubuntu components, so `bianbu.yaml` sets `components: [main, universe, restricted, multiverse]` at the `repo:` level and overrides `repo_suite` in each release block. `preferences` is rarely needed — only when a vendor archive must outrank the distro for a given `(origin, suite)` pair. Each list entry becomes one stanza: From cd4520d9ca5aa0c66c80500429277f28986f7a54 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Sun, 12 Apr 2026 22:02:03 +0200 Subject: [PATCH 10/13] desktops: repo.suite accepts a list for multi-source archives --- docs/Developer-Guide_Desktops.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Developer-Guide_Desktops.md b/docs/Developer-Guide_Desktops.md index 0213584c..ae138b00 100644 --- a/docs/Developer-Guide_Desktops.md +++ b/docs/Developer-Guide_Desktops.md @@ -162,7 +162,7 @@ Use the per-arch layer for permanent arch-wide holes (e.g. `blender` always miss | `url` | string | Base URL for `deb [signed-by=...] `. | | `key_url` | string | URL to the GPG key (ASCII-armored). | | `keyring` | string | Path to the dearmored keyring file, e.g. `/usr/share/keyrings/neon.gpg`. | -| `suite` | string (optional) | Suite path that follows the URL in the source line. Defaults to the release codename (e.g. `noble`). Regex-validated to `^[A-Za-z0-9._/-]+$`. Per-release override: `releases..repo_suite`. | +| `suite` | string or list of strings (optional) | Suite path(s) that follow the URL. A list emits one `deb [...]` line per entry — all sharing url/keyring/components — for vendors whose archive spans multiple parallel suites (base, -security, -updates, -porting, -customization, …). Defaults to the release codename. Regex-validated to `^[A-Za-z0-9._/-]+$`. Per-release override: `releases..repo_suite`. | | `components` | list (optional) | Components that follow the suite. Defaults to `[main]`. Each entry regex-validated to `^[A-Za-z0-9._-]+$`; invalid entries are dropped with a warning. Per-release override: `releases..repo_components`. | | `preferences` | list (optional) | APT pin preferences written to `/etc/apt/preferences.d/`. Each entry needs `origin`, `suite`, and `priority` (positive integer). Removed on uninstall. | From cf834af183765d5db4993a917aa4fe09080357c1 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Thu, 16 Apr 2026 11:59:57 +0200 Subject: [PATCH 11/13] desktops: document mode=build parameter + tag lifecycle steps [B]/[R] Adds the `mode=build` parameter to the command synopsis and the install lifecycle table. Each step is now tagged: [B] = runs in both modes (build + runtime) [R] = runtime-only (skipped when mode=build) Explains when/why mode=build is used (image-build time, no user) and how the first boot inherits skel + graphical.target. Matches armbian/configng#859 (implementation) and armbian/build#9683 (build framework consumer). --- docs/Developer-Guide_Desktops.md | 59 +++++++++++++++++--------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/docs/Developer-Guide_Desktops.md b/docs/Developer-Guide_Desktops.md index ae138b00..89bca25a 100644 --- a/docs/Developer-Guide_Desktops.md +++ b/docs/Developer-Guide_Desktops.md @@ -447,14 +447,14 @@ All functions are loaded by configng's module loader. They share global state (` ### module_desktops ```text -module_desktops [de=] [tier=] [arch=] [release=] +module_desktops [de=] [tier=] [arch=] [release=] [mode=] ``` -Top-level dispatcher. The `de=`, `tier=`, `arch=`, `release=` arguments are parsed positionally from `$@`. +Top-level dispatcher. The `de=`, `tier=`, `arch=`, `release=`, `mode=` arguments are parsed positionally from `$@`. | Command | Behavior | Required args | |---|---|---| -| `install` | Full install pipeline (see [Lifecycle](#lifecycle-install)). Bails out cleanly on `pkg_install` failure without changing system state. | `de=`, `tier=` | +| `install` | Full install pipeline (see [Lifecycle](#lifecycle-install)). Bails out cleanly on `pkg_install` failure without changing system state. With `mode=build`: skips user detection, group membership, skel propagation, and DM start/autologin — intended for image-build time when no real user exists. | `de=`, `tier=` (optional: `mode=build`) | | `remove` | Disables auto-login, stops the display manager, purges every package recorded in `.packages`, runs `pkg_clean`, switches `default.target` back to `multi-user`, isolates to multi-user.target so the running session also drops to console. | `de=` | | `upgrade` | Move an installed desktop to a higher tier. Refuses if the target is the same or lower (use `downgrade`). | `de=`, `tier=` | | `downgrade` | Move an installed desktop to a lower tier. Removable set is intersected with the install manifest so user-installed packages are never touched. | `de=`, `tier=` | @@ -602,33 +602,38 @@ Not called from the desktop install path by default. The `armbian-imager` AppIma The install pipeline in `module_desktops install` is intentionally linear and idempotent-friendly. **Every step that touches system state is gated on the previous step's success.** +Steps marked with `[R]` are **runtime-only** — skipped when `mode=build` is passed (image build time, no real user exists). Steps marked with `[B]` run in **both** modes. + ```{ .text linenums="0" } -1. Validate args de= and tier= both required; tier must be minimal|mid|full -2. Resolve target user module_desktop_getuser -3. Parse YAML at target tier module_desktop_yamlparse $de $arch $release $tier -4. Validate package list exit if DESKTOP_PACKAGES / DESKTOP_PRIMARY_PKG empty -5. Warn on unsupported DESKTOP_SUPPORTED != yes → stderr warning, continue -6. Suppress encfs prompt debconf-set-selections -7. Configure custom repo module_desktop_repo $de (no-op if no repo: block) -8. apt update pkg_update -9. Reset ACTUALLY_INSTALLED array used by pkg_install to record new packages -10. apt install desktop pkgs pkg_install $DESKTOP_PACKAGES ← bail on failure -11. apt install + register DM pkg_install $DESKTOP_DM ← bail on failure - /etc/X11/default-display-manager -12. (Armbian) install plymouth if /etc/apt/sources.list.d/armbian.{list,sources} present -13. Save install manifest /etc/armbian/desktop/.packages and .tier -14. Purge unwanted packages apt-get remove --purge $DESKTOP_PACKAGES_UNINSTALL -15. Install branding module_desktop_branding $de -16. Add user to groups sudo netdev audio video dialout plugdev input bluetooth systemd-journal ssh -17. Profile sync daemon (psd) touch ~/.activate_psd, sudoers entry -18. Sync skel to existing users module_update_skel install (with chown -R safety net) -19. Stop other DMs gdm3/lightdm/sddm one by one -20. Start display manager systemctl start display-manager ← container path skips this -21. Switch default.target systemctl set-default graphical.target ONLY if step 20 succeeded -22. Enable auto-login module_desktops auto de=$de + 1. [B] Validate args de= and tier= both required; tier must be minimal|mid|full + 2. [R] Resolve target user module_desktop_getuser (skipped in mode=build) + 3. [B] Parse YAML at target tier module_desktop_yamlparse $de $arch $release $tier + 4. [B] Validate package list exit if DESKTOP_PACKAGES / DESKTOP_PRIMARY_PKG empty + 5. [B] Warn on unsupported DESKTOP_SUPPORTED != yes → stderr warning, continue + 6. [B] Suppress interactive debconf-set-selections + DEBIAN_FRONTEND=noninteractive + 7. [B] Configure custom repo module_desktop_repo $de (no-op if no repo: block) + 8. [B] Write apt pin _module_desktops_write_apt_pin (force apt.armbian.com .debs) + 9. [B] apt update pkg_update +10. [B] Reset ACTUALLY_INSTALLED array used by pkg_install to record new packages +11. [B] apt install desktop pkgs pkg_install $DESKTOP_PACKAGES ← bail on failure +12. [B] apt install + register DM pkg_install $DESKTOP_DM ← bail on failure + /etc/X11/default-display-manager +13. [B] (Armbian) install plymouth if /etc/apt/sources.list.d/armbian.{list,sources} present +14. [B] Save install manifest /etc/armbian/desktop/.packages and .tier +15. [B] Purge unwanted packages apt-get remove --purge $DESKTOP_PACKAGES_UNINSTALL +16. [B] Install branding module_desktop_branding $de (browser policies, VPU flags, etc.) +17. [R] Add user to groups sudo netdev audio video dialout plugdev input bluetooth systemd-journal ssh +18. [R] Profile sync daemon (psd) touch ~/.activate_psd, sudoers entry +19. [R] Sync skel to existing users module_update_skel install (with chown -R safety net) +20. [R] Stop other DMs gdm3/lightdm/sddm one by one +21. [R] Start display manager systemctl start display-manager ← container path also skips +22. [R] Switch default.target systemctl set-default graphical.target ONLY if step 21 succeeded +23. [R] Enable auto-login module_desktops auto de=$de ``` -If step 10 or 11 fails, the function returns 1 with no further state changes — the manifest is not written, `default.target` stays at `multi-user`, no DM is started. The system is in the same state as if the install had never run. +**`mode=build`** is used by the Armbian build framework at image-creation time. At that point the rootfs has no regular user (armbian-firstrun creates the first user on first boot), and DM/systemd operations make no sense inside a chroot. The packages, branding, manifests, and `/etc/skel` all land correctly; the first user inherits skel at `useradd` time and armbian-firstrun manages `graphical.target`. + +If step 11 or 12 fails, the function returns 1 with no further state changes — the manifest is not written, `default.target` stays at `multi-user`, no DM is started. The system is in the same state as if the install had never run. ## Lifecycle: remove From 81a45285037ccd7113ca4c41fcb9bc77863f7692 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Fri, 17 Apr 2026 11:30:03 +0200 Subject: [PATCH 12/13] desktops: document status tri-state + availability/status filter flags - Document the community tier in the status field (editorial label joins supported / community / unsupported). - Rename DESKTOP_SUPPORTED -> DESKTOP_AVAILABLE throughout, explain it is the computed release+arch-declared axis, distinct from editorial status. - Document parse_desktop_yaml.py's --filter available|unavailable|all and --status flags, plus the reshaped JSON list shape. - Document module_desktops supported's filter= and status= params. - Note the short menu slot allocation (*01-*04) used for community [CSC] DEs, and that unsupported DEs stay out of the menu. - Refresh the audit.py skipped_desktops example now that community is its own tier (only unsupported DEs are skipped). --- docs/Developer-Guide_Desktops.md | 59 +++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/docs/Developer-Guide_Desktops.md b/docs/Developer-Guide_Desktops.md index 89bca25a..958eb4a7 100644 --- a/docs/Developer-Guide_Desktops.md +++ b/docs/Developer-Guide_Desktops.md @@ -107,7 +107,7 @@ Each desktop is defined in a single YAML file under `tools/modules/desktops/yaml | `name` | string | informational | Human-readable name. | | `description` | string | informational | One-line summary, exposed via `DESKTOP_DESC`. | | `display_manager` | string | yes | Greeter package: `gdm3`, `sddm`, `lightdm`, or `none`. | -| `status` | string | yes | `supported` or `unsupported`. Reported via `DESKTOP_STATUS`. Affects only labelling — does not block install. | +| `status` | string | yes | Editorial label — one of `supported`, `community`, `unsupported`. Reported via `DESKTOP_STATUS`. Affects only labelling and catalog filtering — does not block install. `community` is used for DEs that work but are maintained on a best-effort basis; `unsupported` for DEs that are known-broken or not vetted. | | `tiers` | mapping | yes | Per-tier package lists, keyed by `minimal`, `mid`, `full`. See [Tier blocks](#tier-blocks). | | `tier_overrides` | mapping | optional | Per-arch and/or per-release-per-arch package removals (and additions) for tier holes. See [tier_overrides](#tier-overrides). | | `releases` | mapping | yes | Per-release overrides keyed by release codename (`bookworm`, `trixie`, `noble`, `plucky`, ...). | @@ -129,7 +129,7 @@ The release block is **orthogonal** to the tier walk: it applies to whatever tie | Field | Type | Description | |---|---|---| -| `architectures` | list | Architectures supported on this release. Used to compute `DESKTOP_SUPPORTED`. | +| `architectures` | list | Architectures supported on this release. Used to compute `DESKTOP_AVAILABLE` (the "does this YAML declare the requested release+arch combo?" bool — distinct from the editorial `status` above). | | `packages` | list | Extra packages added on top of the tier-resolved set. | | `packages_remove` | list | Packages filtered out of the merged install list. | | `packages_uninstall` | list | Packages purged after install on this release only. | @@ -250,7 +250,7 @@ tiers: name: kde-neon description: "KDE Neon - latest Plasma from KDE repos (Ubuntu only)" display_manager: sddm -status: unsupported +status: supported repo: url: "http://archive.neon.kde.org/testing" key_url: "https://archive.neon.kde.org/public.key" @@ -385,16 +385,26 @@ Single-purpose CLI that bash modules invoke via `python3`. All YAML parsing and # --tier is mandatory. parse_desktop_yaml.py --tier -# List all desktops as TSV (namestatussupportedarchs) -parse_desktop_yaml.py --list +# List all desktops as TSV (namestatusavailablearchs) +# The third column is "yes"/"no" for the computed DESKTOP_AVAILABLE. +parse_desktop_yaml.py --list \ + [--filter ] \ + [--status ] -# Same as --list but JSON-formatted -parse_desktop_yaml.py --list-json +# Same as --list but JSON-formatted. +parse_desktop_yaml.py --list-json \ + [--filter ] \ + [--status ] # Print "\t" for every desktop, used by `installed` parse_desktop_yaml.py --primaries ``` +The two filter flags on `--list` / `--list-json` select on two orthogonal axes, both default to permissive (backwards-compatible with pre-filter callers): + +- `--filter` selects on the **computed** `DESKTOP_AVAILABLE` axis (does the YAML declare this release+arch combo?). Values: `available` (default — hides DEs without an entry for this combo), `unavailable` (only the non-declared DEs), or `all` (no filtering on this axis). +- `--status` selects on the **editorial** `DESKTOP_STATUS` axis. Takes a comma-separated *keep-list* of status values to retain. Omit the flag to keep all statuses. Example: `--status supported,community` drops `unsupported` DEs from the output. + ### Variables emitted (per-desktop mode) All values are double-quoted and shell-escaped via `shell_escape()` (escapes `\`, `"`, `$`, and `` ` ``), so the bash caller can safely `eval` the output. @@ -405,8 +415,8 @@ All values are double-quoted and shell-escaped via `shell_escape()` (escapes `\` | `DESKTOP_PACKAGES_UNINSTALL` | minimal-tier `packages_uninstall` from common + DE + release | Space-separated. | | `DESKTOP_PRIMARY_PKG` | first DE-specific package (not from common) that survives all filters | Used by `module_desktops status` for `dpkg -l` checks. | | `DESKTOP_DM` | `display_manager`, default `lightdm` | | -| `DESKTOP_STATUS` | `status`, default `unsupported` | | -| `DESKTOP_SUPPORTED` | `yes` if `arch` is in the release's `architectures` and `release` is a key in `releases`, else `no` | | +| `DESKTOP_STATUS` | editorial `status` from the YAML, default `unsupported`. One of `supported` / `community` / `unsupported`. | Orthogonal to `DESKTOP_AVAILABLE` — a community DE may be available on a combo (its YAML declares the release+arch) or not. | +| `DESKTOP_AVAILABLE` | `yes` if `arch` is in the release's `architectures` and `release` is a key in `releases`, else `no` | Computed axis — whether the YAML declares this release+arch combo. Named `DESKTOP_SUPPORTED` before 2026-04 (the rename disambiguates this from the editorial `status` field). | | `DESKTOP_DESC` | `description`, default `de_name` | | | `DESKTOP_TIER` | the requested tier name | Set verbatim from the `--tier` arg. | | `DESKTOP_REPO_URL` | `repo.url` | Only emitted when `repo:` exists. | @@ -438,7 +448,20 @@ The parser is strict about top-level structure but tolerant of malformed sub-nod ### List and JSON list modes -Iterates every `*.yaml` (excluding `common.yaml`), parses each one's release block, and prints **only entries supported on the requested (release, arch)**. Used by `module_desktops install` to show available desktops on error and by `module_desktops supported` to expose a machine-readable catalog. These modes do not require `--tier`. +Iterates every `*.yaml` (excluding `common.yaml`), parses each one's release block, and emits one row per DE. By default only entries with `DESKTOP_AVAILABLE=yes` for the requested `(release, arch)` are printed — pass `--filter unavailable` or `--filter all` to override. Pass `--status ` to additionally narrow by the editorial `status` field. Used by `module_desktops install` to show available desktops on error and by `module_desktops supported` to expose a machine-readable catalog. These modes do not require `--tier`. + +Each JSON entry has this shape (two orthogonal status axes): + +```json +{ + "name": "budgie", + "description": "Budgie - elegant desktop from Solus project", + "display_manager": "lightdm", + "status": "community", + "available": true, + "architectures": ["arm64", "amd64"] +} +``` ## Bash module API @@ -467,7 +490,7 @@ Top-level dispatcher. The `de=`, `tier=`, `arch=`, `release=`, `mode=` arguments | `auto` | Configures auto-login for `DESKTOP_DM` (gdm3/sddm/lightdm). Edits the gdm config in place — never overwrites the file — so user customization is preserved. | `de=` | | `manual` | Reverts auto-login. Idempotent. | `de=` | | `login` | Returns 0 if auto-login is currently configured. Anchored regex; safely ignores commented sample lines in the stock noble `custom.conf`. | `de=` | -| `supported` | With `de=`: prints `true`/`false`. Without `de=`: prints JSON catalog of supported desktops. | optional | +| `supported` | With `de=`: prints `true`/`false` based on `DESKTOP_AVAILABLE` for the DE on `arch=`/`release=`. Without `de=`: prints a JSON catalog. Two optional filter knobs: `filter=available\|unavailable\|all` (computed-availability axis, default `available`) and `status=` (editorial-status keep-list — e.g. `status=supported,community` hides editorially `unsupported` DEs). | optional `de=`, `arch=`, `release=`, `filter=`, `status=` | | `installed` | Returns 0 if any desktop is installed (uses cached `--primaries` lookup). | — | | `help` | Shows help and exits. | — | @@ -524,7 +547,7 @@ Calls the parser with `--list` and prints TSV to stdout. Used to assemble the "A module_desktop_supported [arch] [release] ``` -Convenience wrapper around `module_desktop_yamlparse` that returns 0/1 based on `DESKTOP_SUPPORTED`. Suppresses parser stderr — meant for predicates and CI gates. +Convenience wrapper around `module_desktop_yamlparse` that returns 0/1 based on `DESKTOP_AVAILABLE` (the computed-availability axis). Suppresses parser stderr — meant for predicates and CI gates. Note: this function does not consider the editorial `DESKTOP_STATUS` axis — a DE with `status: unsupported` can still return 0 here if its YAML declares the requested release+arch. Filter on `DESKTOP_STATUS` separately if you need to exclude unsupported DEs. ### module_desktop_repo @@ -609,7 +632,7 @@ Steps marked with `[R]` are **runtime-only** — skipped when `mode=build` is pa 2. [R] Resolve target user module_desktop_getuser (skipped in mode=build) 3. [B] Parse YAML at target tier module_desktop_yamlparse $de $arch $release $tier 4. [B] Validate package list exit if DESKTOP_PACKAGES / DESKTOP_PRIMARY_PKG empty - 5. [B] Warn on unsupported DESKTOP_SUPPORTED != yes → stderr warning, continue + 5. [B] Warn on unavailable DESKTOP_AVAILABLE != yes → stderr warning, continue 6. [B] Suppress interactive debconf-set-selections + DEBIAN_FRONTEND=noninteractive 7. [B] Configure custom repo module_desktop_repo $de (no-op if no repo: block) 8. [B] Write apt pin _module_desktops_write_apt_pin (force apt.armbian.com .debs) @@ -719,7 +742,7 @@ This makes the same code path usable for image preseeding inside Docker without done ``` - All `DESKTOP_*` variables should print, `DESKTOP_SUPPORTED="yes"` for any (release, arch) pair you listed in the YAML, and `DESKTOP_TIER` should match the requested tier. + All `DESKTOP_*` variables should print, `DESKTOP_AVAILABLE="yes"` for any (release, arch) pair you listed in the YAML, and `DESKTOP_TIER` should match the requested tier. 8. **List-mode sanity check:** @@ -756,6 +779,8 @@ This makes the same code path usable for image preseeding inside Docker without The `*07-*09` change-tier entries use `module_desktops set-tier` and gate visibility with `module_desktops status de= && ! module_desktops at-tier de= tier=`. + **`status: community` DEs** (the `[CSC]` tier) follow a shorter allocation — only `*01` (install minimal), `*02` (uninstall), `*03` (autologin), `*04` (manual-login) — matching the `kde-neon` precedent. No 3-tier install, no set-tier. Description and `short` carry a trailing `[CSC]` marker so the UI can distinguish community DEs from first-class supported ones. Do NOT add menu entries for `status: unsupported` DEs — they're intentionally kept out of the dialog so users never land on a broken install path from the menu. + ## Matrix audit automation The desktop matrix covers several DEs × several releases × several architectures, and two kinds of drift tend to accumulate silently: @@ -794,12 +819,12 @@ Report shape (`audit-report.json`): "build_distributions": { "": { "name": "...", "support": "supported|csc|eos", "architectures": [...] } }, "missing_releases": [ { "release": "resolute", "support_status": "csc", "architectures": [...] } ], "package_holes": [ { "de": "xfce", "release": "trixie", "arch": "riscv64", "tier": "full", "missing": ["libfoo"] } ], - "skipped_desktops": ["bianbu", "budgie", "deepin", "kde-neon"], - "stats": { "desktops": 8, "scope": 4, "holes": 0, "package_lookups": 0 } + "skipped_desktops": ["bianbu"], + "stats": { "desktops": 11, "scope": 4, "holes": 0, "package_lookups": 0 } } ``` -Desktops with `status: unsupported` in their YAML are listed in `skipped_desktops` and not audited — drift in an unsupported DE isn't actionable. +Desktops with `status: unsupported` in their YAML are listed in `skipped_desktops` and not audited — drift in an unsupported DE isn't actionable. `status: community` DEs **are** audited (drift in a community-tier DE is still worth reporting, even if a maintainer may choose not to act on it immediately). Flags: `--tier {minimal,mid,full}` narrows the scope; `--release ` audits a single release; `--skip-network` is a dry-run that only reports `missing_releases`. From dcfcd4c7a7346fb1e948b65bc54cb8811aebbb0b Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Sun, 19 Apr 2026 17:14:46 +0200 Subject: [PATCH 13/13] desktops: refresh browser map, tier_overrides, remove lifecycle, release list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four corrections after the build framework / CI work: 1. Browser virtual table was stuck on a pre-apt.armbian.com draft where noble and plucky both used epiphany-browser on every arch. Reality: amd64 uses google-chrome-stable, arm64/armhf use chromium from apt.armbian.com (Ubuntu's chromium deb is a snap-shim), Debian riscv64 uses firefox-esr, Ubuntu riscv64 falls back to epiphany-browser. Also document the loong64/sid entry and drop the plucky block entirely — plucky is EOS, resolute replaced it. 2. tier_overrides example was built around plucky. Replace with the current common.yaml shape: arch-wide strip of armbian-imager (armhf/riscv64/loong64) at mid, arch-wide strip of 'code' (armhf pre-t64 .deb + no riscv64 upstream) at full, and the current per-release thunderbird-on-armhf-or-riscv64 holes. 3. Remove lifecycle step 9 was documented as 'pkg_remove → apt-get autopurge'. The remove path now calls apt-get purge directly after filtering out any package apt flags as essential-breaking from a dry-run. Update the step list and add a paragraph explaining why autopurge was dropped (t64-era orphan-cascade into e2fsprogs) and why a dry-run-filter is still needed (some base images ship without e2fsprogs, so the DE install pulls it in and the purge list would otherwise target it). 4. Release list in overview, schema table, packages_uninstall pitfall, and audit report example all mentioned plucky. Replace with resolute/forky/sid/jammy to match the actual release set the inventory declares in 2026. --- docs/Developer-Guide_Desktops.md | 116 +++++++++++++++++++++---------- 1 file changed, 78 insertions(+), 38 deletions(-) diff --git a/docs/Developer-Guide_Desktops.md b/docs/Developer-Guide_Desktops.md index 958eb4a7..5663d954 100644 --- a/docs/Developer-Guide_Desktops.md +++ b/docs/Developer-Guide_Desktops.md @@ -14,8 +14,8 @@ The submodule provides: - **Per-install manifest** — every install records exactly which packages it added so removal and downgrades only undo what they themselves did. - **Custom APT repositories**, branding, group memberships, and skel sync. - **Auto-login** management for `gdm3`, `sddm`, and `lightdm`, with non-destructive in-place edits of the underlying config files. -- **Per-release / per-arch package overrides** so the same YAML works across Debian bookworm/trixie and Ubuntu noble/plucky on amd64/arm64/armhf/riscv64 with different package availability. -- **Browser virtual token** that resolves per-release-per-arch (chromium on Debian, epiphany-browser on Ubuntu, firefox-esr on Debian riscv64, …). +- **Per-release / per-arch package overrides** so the same YAML works across Debian bookworm/trixie/forky/sid and Ubuntu jammy/noble/resolute on amd64/arm64/armhf/riscv64/loong64 with different package availability. +- **Browser virtual token** that resolves per-release-per-arch (google-chrome-stable on amd64, chromium on Debian/Ubuntu arm arches, firefox-esr on Debian riscv64, epiphany-browser on Ubuntu riscv64, …). - **Container/CI awareness** so the same code path can be used inside Docker without trying to start a display manager. ## Tier model @@ -110,7 +110,7 @@ Each desktop is defined in a single YAML file under `tools/modules/desktops/yaml | `status` | string | yes | Editorial label — one of `supported`, `community`, `unsupported`. Reported via `DESKTOP_STATUS`. Affects only labelling and catalog filtering — does not block install. `community` is used for DEs that work but are maintained on a best-effort basis; `unsupported` for DEs that are known-broken or not vetted. | | `tiers` | mapping | yes | Per-tier package lists, keyed by `minimal`, `mid`, `full`. See [Tier blocks](#tier-blocks). | | `tier_overrides` | mapping | optional | Per-arch and/or per-release-per-arch package removals (and additions) for tier holes. See [tier_overrides](#tier-overrides). | -| `releases` | mapping | yes | Per-release overrides keyed by release codename (`bookworm`, `trixie`, `noble`, `plucky`, ...). | +| `releases` | mapping | yes | Per-release overrides keyed by release codename (`bookworm`, `trixie`, `forky`, `sid`, `jammy`, `noble`, `resolute`, ...). | | `repo` | mapping | optional | Custom APT repository, see below. | ### Tier blocks @@ -305,37 +305,66 @@ tiers: browser: bookworm: - amd64: chromium + amd64: google-chrome-stable arm64: chromium armhf: chromium + # bookworm has no riscv64 port — no entry needed trixie: - amd64: chromium + amd64: google-chrome-stable arm64: chromium armhf: chromium - riscv64: firefox-esr # 'firefox' does not exist in Debian + riscv64: firefox-esr # 'firefox' does not exist in Debian noble: - amd64: epiphany-browser # 'chromium' deb is a snap-shim - arm64: epiphany-browser - armhf: epiphany-browser - riscv64: epiphany-browser - plucky: - amd64: epiphany-browser - arm64: epiphany-browser - armhf: epiphany-browser + amd64: google-chrome-stable + arm64: chromium # apt.armbian.com real .deb (Ubuntu's is snap-shim) + armhf: chromium + riscv64: epiphany-browser # firefox/chromium not built for Ubuntu riscv64 + resolute: + amd64: google-chrome-stable + arm64: chromium + armhf: chromium riscv64: epiphany-browser + forky: + amd64: google-chrome-stable + arm64: chromium + armhf: chromium + riscv64: firefox-esr + sid: + amd64: google-chrome-stable + arm64: chromium + armhf: chromium + riscv64: firefox-esr + loong64: firefox-esr # chromium not yet built for loong64 tier_overrides: mid: + # armbian-imager ships only amd64/arm64 upstream — strip it on + # every other arch, across every release. + architectures: + armhf: { packages_remove: [armbian-imager] } + riscv64: { packages_remove: [armbian-imager] } + loong64: { packages_remove: [armbian-imager] } releases: bookworm: architectures: amd64: { packages_remove: [loupe] } # GNOME 43 era — no loupe arm64: { packages_remove: [loupe] } armhf: { packages_remove: [loupe] } - plucky: + jammy: architectures: - armhf: { packages_remove: [loupe] } # dropped on plucky/armhf + amd64: { packages_remove: [loupe] } # GNOME 42 — no loupe + arm64: { packages_remove: [loupe] } + armhf: { packages_remove: [loupe] } + riscv64: { packages_remove: [loupe] } full: + # The 'code' (VSCode) .deb on apt.armbian.com links against the + # pre-t64 library names, which don't exist on post-t64 releases + # (trixie+, noble+). amd64/arm64 were rebuilt; armhf wasn't. No + # riscv64 upstream build exists at all. Strip arch-wide on both + # until/unless the armhf .deb is refreshed. + architectures: + armhf: { packages_remove: [code] } + riscv64: { packages_remove: [code] } releases: bookworm: architectures: @@ -344,17 +373,14 @@ tier_overrides: architectures: armhf: { packages_remove: [thunderbird] } noble: - # thunderbird on Ubuntu is a snap-shim that requires snapd - # which Armbian doesn't ship — strip it on every arch. + # thunderbird on Ubuntu noble armhf/riscv64 is absent (no + # upstream Ubuntu deb there), so strip on those two arches + # only. amd64/arm64 get the real .deb from apt.armbian.com. architectures: - amd64: { packages_remove: [thunderbird] } - arm64: { packages_remove: [thunderbird] } armhf: { packages_remove: [thunderbird] } riscv64: { packages_remove: [thunderbird] } - plucky: + resolute: architectures: - amd64: { packages_remove: [thunderbird] } - arm64: { packages_remove: [thunderbird] } armhf: { packages_remove: [thunderbird] } riscv64: { packages_remove: [thunderbird] } ``` @@ -370,9 +396,12 @@ The literal string `browser` inside any tier block resolves to a real package na The per-release layer is needed because the same arch can resolve differently across releases: - Debian has `firefox-esr` but **no** `firefox` package. -- Ubuntu's `chromium` deb is a snap-shim wrapper that requires `snapd`. Armbian doesn't ship snapd, so the shim is broken at runtime — substitute a real GTK browser instead. `epiphany-browser` (GNOME Web) is small, native deb, and available on every Ubuntu arch. +- Ubuntu's `chromium` / `firefox` debs are snap-shim wrappers that require `snapd`. Armbian doesn't ship snapd, so the shims are broken at runtime — apt.armbian.com hosts real `chromium` / `firefox` / `google-chrome-stable` .debs used instead. +- amd64 always gets `google-chrome-stable` (Google publishes no arm/riscv builds, so this is amd64-only). - `chromium` isn't built for riscv64 in either Debian or Ubuntu. -- `firefox` isn't built for noble/plucky riscv64 either. +- Ubuntu doesn't publish `firefox` or `firefox-esr` for riscv64 (Mozilla has no riscv64 binaries, and `firefox-esr` is a Debian-only package name). Fall back to `epiphany-browser` (GNOME Web) there — native GTK, small, and available on every Ubuntu arch. +- Debian riscv64 gets `firefox-esr` because the Debian archive does publish it for riscv64. +- `loong64` is only declared for `sid` in the inventory; `chromium` isn't built there yet either, so it uses `firefox-esr`. ## Python helper: parse_desktop_yaml.py @@ -661,24 +690,35 @@ If step 11 or 12 fails, the function returns 1 with no further state changes — ## Lifecycle: remove ```{ .text linenums="0" } -1. Validate args de= required -2. Read installed tier marker /etc/armbian/desktop/.tier (default: minimal) -3. Parse YAML at the installed module_desktop_yamlparse $de $arch $release $installed_tier + 1. Validate args de= required + 2. Read installed tier marker /etc/armbian/desktop/.tier (default: minimal) + 3. Parse YAML at the installed module_desktop_yamlparse $de $arch $release $installed_tier tier -4. Disable auto-login module_desktops manual de=$de -5. Stop display manager systemctl stop display-manager -6. Switch default.target systemctl set-default multi-user.target -7. Isolate to multi-user systemctl isolate multi-user.target (drops running session + 4. Disable auto-login module_desktops manual de=$de + 5. Stop display manager systemctl stop display-manager + 6. Switch default.target systemctl set-default multi-user.target + 7. Isolate to multi-user systemctl isolate multi-user.target (drops running session to console immediately, no reboot needed) -8. Compute removable set from /etc/armbian/desktop/.packages + 8. Compute removable set from /etc/armbian/desktop/.packages fallback: walk DESKTOP_PACKAGES through dpkg-query -9. pkg_remove apt-get autopurge -10. Delete manifest files rm /etc/armbian/desktop/.{packages,tier} -11. pkg_clean apt-get clean — reclaim downloaded .deb cache + 9. Filter out essentials apt-get -s -y purge (simulation, stderr parsed) + every package apt flags under "WARNING: The following + essential packages will be removed" is dropped from + the removable set — see note below +10. Purge remaining set apt-get -y purge ← bail on failure, + manifest preserved for retry +11. Delete manifest files rm /etc/armbian/desktop/.{packages,tier} (only after 10 succeeds) +12. pkg_clean apt-get clean — reclaim downloaded .deb cache ``` The `set-default` and `isolate` calls together ensure the user gets a console login on tty1 immediately after the uninstall, without needing to reboot. Without them, the system stays pinned to `graphical.target` with no DM behind it and the local console is blank. +**Why the essential filter (step 9).** The remove path calls `apt-get -y purge` directly — **not** `pkg_remove` (which wraps `apt-get autopurge`). `autopurge` adds an orphan-cleanup cascade on top of the removal, and on fresh post-t64 images (trixie+, noble+) several shared libs pulled in alongside `e2fsprogs` (`libext2fs2t64`, `libss2`, `logsave`) are marked auto-installed. Once the DE is gone nothing manual depends on them, autopurge proposes to orphan-remove the whole chain, and apt 2.9+ / solver 3.0 vetoes the transaction with `E: Essential packages were removed and -y was used without --allow-remove-essential` — nothing actually gets removed. A plain `apt-get purge` avoids the cascade, and the manifest is already the complete list, so no cascade is needed. + +A separate case the filter catches: some base images (notably `armbian/repository-update:*-armhf` tags rebuilt from debian-slim) ship without `e2fsprogs` pre-installed. When a DE's install pulls in `dracut-install` or `gnome-disk-utility` transitively, those pull `e2fsprogs`, it lands in the manifest, and purging it would touch an Essential package. Step 9 simulates the purge, parses apt's essential-warning block (stripping `(due to X)` annotations), and drops every flagged name from the list before the real purge runs. + +On failure of step 10 the function returns 1 with the manifest left in place, so the next `remove` retries against the same list rather than falling into the less-precise YAML-walk path. + ## Lifecycle: upgrade and downgrade `upgrade` and `downgrade` are the two halves of `_module_desktops_change_tier`: @@ -815,7 +855,7 @@ Report shape (`audit-report.json`): ```json { - "scanned_releases": ["bookworm", "noble", "plucky", "trixie"], + "scanned_releases": ["bookworm", "noble", "resolute", "trixie"], "build_distributions": { "": { "name": "...", "support": "supported|csc|eos", "architectures": [...] } }, "missing_releases": [ { "release": "resolute", "support_status": "csc", "architectures": [...] } ], "package_holes": [ { "de": "xfce", "release": "trixie", "arch": "riscv64", "tier": "full", "missing": ["libfoo"] } ], @@ -890,7 +930,7 @@ If Claude judged the report non-actionable (e.g. the only finding is a `csc`-tie ### packages_uninstall cascade -Listing a package in `tiers.minimal.packages_uninstall` runs `apt-get remove --purge` on it after the install. If that package is a hard `Depends:` of any meta package the DE install pulled in, apt's autoremove cascade will yank the meta package along with it — and on systems with `APT::Get::AutomaticRemove "true"` (Ubuntu noble/plucky), the cascade keeps going and rips out a chunk of the desktop. Real examples that bit us: +Listing a package in `tiers.minimal.packages_uninstall` runs `apt-get remove --purge` on it after the install. If that package is a hard `Depends:` of any meta package the DE install pulled in, apt's autoremove cascade will yank the meta package along with it — and on systems with `APT::Get::AutomaticRemove "true"` (Ubuntu noble/resolute), the cascade keeps going and rips out a chunk of the desktop. Real examples that bit us: - Listing any `xfce4-goodies` plugin (e.g. `xfce4-clipman-plugin`) yanks `xfce4-goodies` itself, then half the desktop. - Listing `language-selector-gnome` yanks `gnome-control-center` (which has it as a hard Depends on Ubuntu), so the user loses Settings.