feat(bootstrap): per-os desktop apps, KDE plasma-login-manager and DM resolution

This commit is contained in:
2026-05-25 04:30:53 +02:00
parent 0185797af9
commit 44f5adc682
7 changed files with 150 additions and 59 deletions

View File

@@ -301,18 +301,27 @@ On distros with older dracut (no `tpm2-tss` module), clevis is used as a fallbac
| ----------------- | ------ | -------------- | ----------------------------------------- | | ----------------- | ------ | -------------- | ----------------------------------------- |
| `enabled` | bool | `false` | Install desktop environment | | `enabled` | bool | `false` | Install desktop environment |
| `environment` | string | `""` | `gnome`, `kde`, `sway`, or `hyprland` | | `environment` | string | `""` | `gnome`, `kde`, `sway`, or `hyprland` |
| `display_manager` | string | auto-detected | Override DM: `gdm`, `sddm`, or `greetd` | | `display_manager` | string | auto-detected | Override DM: `gdm`, `sddm`, `plasma-login-manager`, `greetd`, or `ly` |
| `autologin` | bool \| string | `false` | `false` to disable, or a username from `system.users` to auto-login that user | | `autologin` | bool \| string | `false` | `false` to disable, or a username from `system.users` to auto-login that user |
| `session` | string | auto-from-environment | Session to autologin into; overrides the per-environment default (sddm `.desktop` basename / greetd command) | | `session` | string | auto-from-environment | Session to autologin into; overrides the per-environment default (sddm `.desktop` basename / greetd command) |
| `groups` | list | `[]` | Opt-in package groups installed on top of the base set (keys of `desktop_package_groups`, e.g. `dev`) | | `groups` | list | `[]` | Opt-in package groups installed on top of the base set (keys of `desktop_package_groups`, e.g. `dev`) |
All desktop environments are Wayland-only. `sway` and `hyprland` are available on Arch only; All desktop environments are Wayland-only. `sway` and `hyprland` are available on Arch only;
`gnome` and `kde` are available on all three families. `gnome` and `kde` are available on all three families. On enterprise Linux
(almalinux/rocky/rhel) the base desktop installs browser, PDF and image viewers but no
video player - none is packaged in the EL base repositories, and no third-party repo is
pulled in; add one from rpmfusion/flatpak if you need it.
When `enabled: true`, the bootstrap installs the desktop environment packages, enables the display manager When `enabled: true`, the bootstrap installs the desktop environment packages, enables the display manager
and bluetooth services, and sets the systemd default target to `graphical.target`. and bluetooth services, and sets the systemd default target to `graphical.target`.
Display manager auto-detection: gnome to gdm, kde to sddm, sway to greetd, hyprland to greetd. Display manager auto-detection: gnome to gdm; kde to plasma-login-manager on Arch and
Fedora 44+ (Plasma 6.6), else sddm; sway and hyprland to greetd.
`ly` is an explicit-only override (never auto-selected), available on Arch only,
and is desktop-agnostic - it can front any environment. It runs on `tty2` with
`getty@tty2` masked, and its autologin is written to `/etc/ly/config.ini`; set `session`
to the target session's `.desktop` basename (sway and hyprland resolve automatically).
When `autologin` names a user, the matching display manager is configured to log that user in without a When `autologin` names a user, the matching display manager is configured to log that user in without a
password prompt. `session` is resolved automatically per environment when left empty (gdm picks its default, password prompt. `session` is resolved automatically per environment when left empty (gdm picks its default,

View File

@@ -9,6 +9,8 @@
_family_pkgs: "{{ bootstrap_desktop_packages[os_family] | default({}) }}" _family_pkgs: "{{ bootstrap_desktop_packages[os_family] | default({}) }}"
_de_config: "{{ _family_pkgs[_de] | default({}) }}" _de_config: "{{ _family_pkgs[_de] | default({}) }}"
_base: "{{ bootstrap_desktop_base_packages[os_family] | default([]) }}" _base: "{{ bootstrap_desktop_base_packages[os_family] | default([]) }}"
_dm: "{{ system_cfg.features.desktop.display_manager | default('') }}"
_dm_override_pkg: "{{ (bootstrap_dm_override_packages[_dm] | default({}))[os_family] | default('') }}"
_requested_groups: "{{ system_cfg.features.desktop.groups | default([]) }}" _requested_groups: "{{ system_cfg.features.desktop.groups | default([]) }}"
_group_pkgs: >- _group_pkgs: >-
{{ {{
@@ -20,16 +22,10 @@
| sum(start=[]) | sum(start=[])
}} }}
ansible.builtin.set_fact: ansible.builtin.set_fact:
# GNOME ships under different dnf environment groups: Fedora uses _desktop_groups: "{{ _de_config.groups | default([]) }}"
# workstation-product-environment, enterprise RHEL/Rocky/Alma use
# graphical-server-environment ("Server with GUI").
_desktop_groups: >-
{{ ['graphical-server-environment']
if (_de == 'gnome' and os_family == 'RedHat' and os != 'fedora')
else (_de_config.groups | default([])) }}
_desktop_packages: >- _desktop_packages: >-
{{ {{
((_de_config.packages | default([])) + _base + _group_pkgs) ((_de_config.packages | default([])) + _base + _group_pkgs + [_dm_override_pkg])
| reject('equalto', '') | reject('equalto', '')
| unique | unique
| list | list
@@ -61,7 +57,8 @@
{{ chroot_command }} dnf --releasever={{ os_version_major }} {{ chroot_command }} dnf --releasever={{ os_version_major }}
--setopt=install_weak_deps=False install -y {{ _desktop_packages | join(' ') }} --setopt=install_weak_deps=False install -y {{ _desktop_packages | join(' ') }}
Debian: >- Debian: >-
{{ chroot_command }} apt install -y --install-recommends {{ _desktop_packages | join(' ') }} {{ chroot_command }} env DEBIAN_FRONTEND=noninteractive
apt install -y --install-recommends {{ _desktop_packages | join(' ') }}
Archlinux: >- Archlinux: >-
pacstrap /mnt {{ _desktop_packages | join(' ') }} pacstrap /mnt {{ _desktop_packages | join(' ') }}
ansible.builtin.command: "{{ _install_commands[os_family] }}" ansible.builtin.command: "{{ _install_commands[os_family] }}"

View File

@@ -1,16 +1,41 @@
--- ---
# Per-family desktop environment package definitions.
# Keyed by os_family -> environment -> groups (dnf group install) / packages.
# Wayland only: gnome, kde, sway, hyprland. No X11/xorg-server, no X11-only DEs. # Wayland only: gnome, kde, sway, hyprland. No X11/xorg-server, no X11-only DEs.
# DE sets carry the session bits + the DE-specific xdg portal backend; the
# shared base below (fonts/audio/portal core/power/viewer apps) is layered on # plasma-login-manager on Arch/Fedora44+ (Plasma 6.6), else sddm.
# top for every DE via bootstrap_desktop_base_packages. bootstrap_kde_login_manager: >-
{{
'plasma-login-manager'
if (os == 'archlinux' or (os == 'fedora' and (os_version | int) >= 44))
else 'sddm'
}}
# Native DMs ride in each DE's package set; only explicit non-native overrides
# need a package here. ly is Arch-only (validation rejects it elsewhere first).
bootstrap_dm_override_packages:
ly:
Archlinux: ly
# EL = non-fedora RedHat.
bootstrap_os_is_el: "{{ os in ['almalinux', 'rocky', 'rhel'] }}"
bootstrap_os_is_el10: "{{ bootstrap_os_is_el | bool and (os_version | default('0') | int) >= 10 }}"
# EL10 renames (evince->papers, eog->loupe, ppd->tuned-ppd); fira-code + mpv absent on EL.
bootstrap_desktop_browser: "{{ 'firefox-esr' if os == 'debian' else 'firefox' }}"
bootstrap_desktop_pdf: "{{ 'papers' if bootstrap_os_is_el10 | bool else 'evince' }}"
bootstrap_desktop_image: "{{ 'loupe' if bootstrap_os_is_el10 | bool else 'eog' }}"
bootstrap_desktop_power: "{{ 'tuned-ppd' if bootstrap_os_is_el10 | bool else 'power-profiles-daemon' }}"
bootstrap_desktop_redhat_codefont: "{{ '' if bootstrap_os_is_el | bool else 'fira-code-fonts' }}"
bootstrap_desktop_redhat_video: "{{ '' if bootstrap_os_is_el | bool else 'mpv' }}"
bootstrap_desktop_packages: bootstrap_desktop_packages:
RedHat: RedHat:
gnome: gnome:
groups: groups: []
- workstation-product-environment packages:
packages: [] - gnome-shell
- gnome-control-center
- nautilus
- gnome-session
- gdm
kde: kde:
groups: [] groups: []
packages: packages:
@@ -18,7 +43,7 @@ bootstrap_desktop_packages:
- plasma-nm - plasma-nm
- plasma-pa - plasma-pa
- plasma-systemmonitor - plasma-systemmonitor
- sddm - "{{ bootstrap_kde_login_manager }}"
- konsole - konsole
- dolphin - dolphin
- kate - kate
@@ -41,7 +66,7 @@ bootstrap_desktop_packages:
- plasma-desktop - plasma-desktop
- plasma-nm - plasma-nm
- plasma-pa - plasma-pa
- sddm - "{{ bootstrap_kde_login_manager }}"
- konsole - konsole
- dolphin - dolphin
- kate - kate
@@ -62,7 +87,7 @@ bootstrap_desktop_packages:
- plasma-desktop - plasma-desktop
- plasma-nm - plasma-nm
- plasma-pa - plasma-pa
- sddm - "{{ bootstrap_kde_login_manager }}"
- konsole - konsole
- dolphin - dolphin
- kate - kate
@@ -102,26 +127,23 @@ bootstrap_desktop_packages:
- qt6-wayland - qt6-wayland
- bluez - bluez
# Shared desktop base, installed for EVERY DE whenever desktop.enabled. # Installed for EVERY DE whenever desktop.enabled. No file manager here: DE metas
# Fonts (noto + emoji + one nerd font), audio stack (pipewire + wireplumber + # bundle their own and the wlroots sets above carry nautilus.
# pipewire-pulse), xdg portal core, power-profiles-daemon, and viewer-only base
# apps (browser, PDF/image/video viewers). DE metas (gnome/plasma) bundle their
# own file manager + settings, so no file manager is added here - the wlroots
# DE sets above carry their own (nautilus).
bootstrap_desktop_base_packages: bootstrap_desktop_base_packages:
RedHat: RedHat:
- google-noto-sans-fonts - google-noto-sans-fonts
- google-noto-emoji-fonts - google-noto-emoji-fonts
- fira-code-fonts - "{{ bootstrap_desktop_redhat_codefont }}"
- pipewire - pipewire
- wireplumber - wireplumber
- pipewire-pulseaudio - pipewire-pulseaudio
- xdg-desktop-portal - xdg-desktop-portal
- power-profiles-daemon - "{{ bootstrap_desktop_power }}"
- bluez
- firefox - firefox
- evince - "{{ bootstrap_desktop_pdf }}"
- eog - "{{ bootstrap_desktop_image }}"
- mpv - "{{ bootstrap_desktop_redhat_video }}"
Debian: Debian:
- fonts-noto - fonts-noto
- fonts-noto-color-emoji - fonts-noto-color-emoji
@@ -131,7 +153,8 @@ bootstrap_desktop_base_packages:
- pipewire-pulse - pipewire-pulse
- xdg-desktop-portal - xdg-desktop-portal
- power-profiles-daemon - power-profiles-daemon
- firefox-esr - bluez
- "{{ bootstrap_desktop_browser }}"
- evince - evince
- eog - eog
- mpv - mpv
@@ -144,14 +167,14 @@ bootstrap_desktop_base_packages:
- pipewire-pulse - pipewire-pulse
- xdg-desktop-portal - xdg-desktop-portal
- power-profiles-daemon - power-profiles-daemon
- bluez
- firefox - firefox
- evince - evince
- loupe - loupe
- mpv - mpv
# Opt-in package groups, selected per host via features.desktop.groups (a list # Opt-in groups selected per host via features.desktop.groups; the union of the
# of group names). _desktop.yml installs the union of the requested groups' # requested groups' packages is installed. Empty selection by default.
# packages. Empty selection by default.
desktop_package_groups: desktop_package_groups:
dev: dev:
RedHat: RedHat:

View File

@@ -4,11 +4,20 @@
vars: vars:
_autologin: "{{ system_cfg.features.desktop.autologin | default(false) }}" _autologin: "{{ system_cfg.features.desktop.autologin | default(false) }}"
ansible.builtin.set_fact: ansible.builtin.set_fact:
# KDE resolves to the plasmalogin unit on Arch/Fedora44+ (Plasma 6.6), else sddm.
_desktop_dm: >- _desktop_dm: >-
{{ {{
system_cfg.features.desktop.display_manager ('plasmalogin'
if system_cfg.features.desktop.display_manager == 'plasma-login-manager'
else system_cfg.features.desktop.display_manager)
if (system_cfg.features.desktop.display_manager | length > 0) if (system_cfg.features.desktop.display_manager | length > 0)
else (configuration_desktop_dm_map[system_cfg.features.desktop.environment] | default('')) else (
('plasmalogin'
if (os == 'archlinux' or (os == 'fedora' and (os_version | int) >= 44))
else 'sddm')
if system_cfg.features.desktop.environment == 'kde'
else (configuration_desktop_dm_map[system_cfg.features.desktop.environment] | default(''))
)
}} }}
_desktop_session: "{{ system_cfg.features.desktop.session | default('') }}" _desktop_session: "{{ system_cfg.features.desktop.session | default('') }}"
# Explicit session wins, else the per-environment command. Single source of # Explicit session wins, else the per-environment command. Single source of
@@ -52,6 +61,7 @@
- _configuration_platform.init_system == 'systemd' - _configuration_platform.init_system == 'systemd'
- system_cfg.features.desktop.enabled | bool - system_cfg.features.desktop.enabled | bool
- _desktop_dm | length > 0 - _desktop_dm | length > 0
- _desktop_dm != 'ly'
ansible.builtin.command: "{{ chroot_command }} systemctl enable {{ _desktop_dm }}" ansible.builtin.command: "{{ chroot_command }} systemctl enable {{ _desktop_dm }}"
register: configuration_enable_dm_result register: configuration_enable_dm_result
changed_when: configuration_enable_dm_result.rc == 0 changed_when: configuration_enable_dm_result.rc == 0
@@ -70,14 +80,40 @@
register: _ufw_enable_result register: _ufw_enable_result
changed_when: _ufw_enable_result.rc == 0 changed_when: _ufw_enable_result.rc == 0
failed_when: false failed_when: false
- name: Enable ly on its tty
- name: Set default systemd target to graphical
when: when:
- _configuration_platform.init_system == 'systemd' - _configuration_platform.init_system == 'systemd'
- system_cfg.features.desktop.enabled | bool - system_cfg.features.desktop.enabled | bool
ansible.builtin.command: "{{ chroot_command }} systemctl set-default graphical.target" - _desktop_dm == 'ly'
register: _desktop_target_result vars:
changed_when: _desktop_target_result.rc == 0 _ly_tty: tty2
block:
- name: Enable ly display manager
ansible.builtin.command: "{{ chroot_command }} systemctl enable ly@{{ _ly_tty }}.service"
register: configuration_enable_ly_result
changed_when: configuration_enable_ly_result.rc == 0
failed_when: >-
configuration_enable_ly_result.rc != 0
or 'No such file or directory' in (configuration_enable_ly_result.stderr | default(''))
or 'does not exist' in (configuration_enable_ly_result.stderr | default(''))
# ly drives the VT itself; mask getty so logind never spawns a login on that tty.
- name: Mask getty on ly's tty
ansible.builtin.command: "{{ chroot_command }} systemctl mask getty@{{ _ly_tty }}.service"
register: configuration_mask_getty_result
changed_when: configuration_mask_getty_result.rc == 0
failed_when: >-
configuration_mask_getty_result.rc != 0
and 'No such file or directory' not in (configuration_mask_getty_result.stderr | default(''))
and 'does not exist' not in (configuration_mask_getty_result.stderr | default(''))
- name: Set default systemd target
when: _configuration_platform.init_system == 'systemd'
vars:
_default_target: "{{ 'graphical.target' if system_cfg.features.desktop.enabled | bool else 'multi-user.target' }}"
ansible.builtin.command: "{{ chroot_command }} systemctl set-default {{ _default_target }}"
register: _set_default_target_result
changed_when: _set_default_target_result.rc == 0
- name: Enable PipeWire user services globally - name: Enable PipeWire user services globally
when: when:
@@ -134,8 +170,7 @@
- _desktop_dm == 'gdm' - _desktop_dm == 'gdm'
- _desktop_autologin_user | length > 0 - _desktop_autologin_user | length > 0
vars: vars:
# Debian's gdm3 reads /etc/gdm3/daemon.conf; RedHat/Arch GDM read # Debian gdm3 reads daemon.conf; RedHat/Arch gdm read custom.conf.
# /etc/gdm/custom.conf. The keys are identical, only the path differs.
_gdm_dir: "/mnt/etc/{{ 'gdm3' if os_family == 'Debian' else 'gdm' }}" _gdm_dir: "/mnt/etc/{{ 'gdm3' if os_family == 'Debian' else 'gdm' }}"
_gdm_conf: "{{ 'daemon.conf' if os_family == 'Debian' else 'custom.conf' }}" _gdm_conf: "{{ 'daemon.conf' if os_family == 'Debian' else 'custom.conf' }}"
block: block:
@@ -151,16 +186,20 @@
dest: "{{ _gdm_dir }}/{{ _gdm_conf }}" dest: "{{ _gdm_dir }}/{{ _gdm_conf }}"
mode: "0644" mode: "0644"
- name: Configure SDDM autologin # SDDM and plasma-login-manager share the [Autologin] format and the KDE Wayland
# session; only the config dir differs (sddm.conf.d vs plasmalogin.conf.d).
- name: Configure SDDM / plasma-login-manager autologin
when: when:
- _configuration_platform.init_system == 'systemd' - _configuration_platform.init_system == 'systemd'
- system_cfg.features.desktop.enabled | bool - system_cfg.features.desktop.enabled | bool
- _desktop_dm == 'sddm' - _desktop_dm in ['sddm', 'plasmalogin']
- _desktop_autologin_user | length > 0 - _desktop_autologin_user | length > 0
vars:
_autologin_conf_dir: "/mnt/etc/{{ 'plasmalogin.conf.d' if _desktop_dm == 'plasmalogin' else 'sddm.conf.d' }}"
block: block:
- name: Ensure SDDM config directory exists - name: Ensure KDE login-manager config directory exists
ansible.builtin.file: ansible.builtin.file:
path: /mnt/etc/sddm.conf.d path: "{{ _autologin_conf_dir }}"
state: directory state: directory
mode: "0755" mode: "0755"
@@ -179,8 +218,29 @@
{%- set names = _kde_wayland_sessions.files | map(attribute='path') | map('basename') | list -%} {%- set names = _kde_wayland_sessions.files | map(attribute='path') | map('basename') | list -%}
{{ 'plasma.desktop' if 'plasma.desktop' in names else (names | first | default('')) }} {{ 'plasma.desktop' if 'plasma.desktop' in names else (names | first | default('')) }}
- name: Write SDDM autologin drop-in - name: Write KDE login-manager autologin drop-in
ansible.builtin.template: ansible.builtin.template:
src: sddm-autologin.conf.j2 src: sddm-autologin.conf.j2
dest: /mnt/etc/sddm.conf.d/10-autologin.conf dest: "{{ _autologin_conf_dir }}/10-autologin.conf"
mode: "0644" mode: "0644"
# ly ships a flat (sectionless) config.ini; edit it in place to keep upstream
# defaults. Both keys are required: an unresolved session writes 'null', which
# disables autologin rather than leaving it half-configured.
- name: Configure ly autologin
when:
- _configuration_platform.init_system == 'systemd'
- system_cfg.features.desktop.enabled | bool
- _desktop_dm == 'ly'
- _desktop_autologin_user | length > 0
community.general.ini_file:
path: /mnt/etc/ly/config.ini
option: "{{ item.key }}"
value: "{{ item.value }}"
create: false
mode: "0644"
loop:
- key: auto_login_user
value: "{{ _desktop_autologin_user }}"
- key: auto_login_session
value: "{{ _greetd_session if (_greetd_session | length > 0) else 'null' }}"

View File

@@ -43,9 +43,7 @@ configuration_desktop_dm_map:
sway: greetd sway: greetd
hyprland: greetd hyprland: greetd
# Per-environment session command for greetd-launched compositors (sway/hyprland): # greetd session commands for sway/hyprland (gnome/kde use a DM instead).
# the executable greetd starts. kde/gnome use a display manager (sddm/gdm) whose
# Wayland session is resolved separately, so they are not in this map.
configuration_desktop_session_cmd_map: configuration_desktop_session_cmd_map:
sway: sway sway: sway
hyprland: Hyprland hyprland: Hyprland

View File

@@ -128,7 +128,7 @@ system_defaults:
desktop: desktop:
enabled: false enabled: false
environment: "" # gnome|kde|sway|hyprland environment: "" # gnome|kde|sway|hyprland
display_manager: "" # auto from environment when empty; override: gdm|sddm|greetd display_manager: "" # auto from environment when empty; override: gdm|sddm|greetd|plasma-login-manager|ly
autologin: false # false | username from system.users autologin: false # false | username from system.users
session: "" # session name/command for the autologin user session: "" # session name/command for the autologin user
groups: [] # opt-in package groups (keys of desktop_package_groups) groups: [] # opt-in package groups (keys of desktop_package_groups)

View File

@@ -262,7 +262,7 @@
or os_family_map[os] | default('') == "Archlinux" or os_family_map[os] | default('') == "Archlinux"
- >- - >-
system_cfg.features.desktop.display_manager | default('') | length == 0 system_cfg.features.desktop.display_manager | default('') | length == 0
or system_cfg.features.desktop.display_manager in ["gdm", "sddm", "greetd", "plasma-login-manager"] or system_cfg.features.desktop.display_manager in ["gdm", "sddm", "greetd", "plasma-login-manager", "ly"]
- >- - >-
system_cfg.features.desktop.display_manager | default('') != "greetd" system_cfg.features.desktop.display_manager | default('') != "greetd"
or system_cfg.features.desktop.environment in ["sway", "hyprland"] or system_cfg.features.desktop.environment in ["sway", "hyprland"]
@@ -275,6 +275,9 @@
- >- - >-
system_cfg.features.desktop.display_manager | default('') != "plasma-login-manager" system_cfg.features.desktop.display_manager | default('') != "plasma-login-manager"
or os == "archlinux" or (os == "fedora" and (os_version | int) >= 44) or os == "archlinux" or (os == "fedora" and (os_version | int) >= 44)
- >-
system_cfg.features.desktop.display_manager | default('') != "ly"
or os == "archlinux"
fail_msg: >- fail_msg: >-
Invalid desktop config: environment '{{ system_cfg.features.desktop.environment }}' Invalid desktop config: environment '{{ system_cfg.features.desktop.environment }}'
for os_family '{{ os_family_map[os] | default('Unknown') }}', for os_family '{{ os_family_map[os] | default('Unknown') }}',
@@ -282,7 +285,8 @@
gnome and kde are available on all families; sway and hyprland are Archlinux only. gnome and kde are available on all families; sway and hyprland are Archlinux only.
display_manager must be empty (auto) or match the environment's native DM: display_manager must be empty (auto) or match the environment's native DM:
gnome->gdm, kde->plasma-login-manager on Arch/Fedora44+ else sddm, gnome->gdm, kde->plasma-login-manager on Arch/Fedora44+ else sddm,
sway/hyprland->greetd. Only that DM's package is installed, so a mismatched sway/hyprland->greetd. ly is an explicit override on Arch only and may
front any environment. Only that DM's package is installed, so a mismatched
override fails at enable time. override fails at enable time.
quiet: true quiet: true