diff --git a/configs/baseline/airootfs/usr/lib/python3.14/site-packages/pyanaconda/ui/gui/spokes/fws_recap.py b/configs/baseline/airootfs/usr/lib/python3.14/site-packages/pyanaconda/ui/gui/spokes/fws_recap.py new file mode 100644 index 0000000..2f97af5 --- /dev/null +++ b/configs/baseline/airootfs/usr/lib/python3.14/site-packages/pyanaconda/ui/gui/spokes/fws_recap.py @@ -0,0 +1,347 @@ +# FWS — Page « Récapitulatif » : écran À PART ENTIÈRE avant l'installation. +# +# POURQUOI ICI (airootfs, pas dans le paquet anaconda) : même mécanisme que la +# spoke « Desktop Environment » (fws_desktop.py) — Anaconda découvre les spokes +# au RUNTIME (collect) dans ce dossier → l'overlay airootfs suffit, AUCUN +# rebuild du paquet anaconda. Le glade associé est dans +# /usr/share/anaconda/ui/spokes/fws_recap.glade (aussi via l'airootfs). +# +# PLACEMENT DANS LE FLUX : standalone spoke « postForHub = SummaryHub » avec +# priority = -10. L'ordre des écrans est pre(SummaryHub) → SummaryHub → +# post(SummaryHub) trié par priority croissante (UserInterface. +# _orderActionClasses). ProgressSpoke — l'écran qui DÉMARRE l'installation +# dans son refresh() — est aussi post(SummaryHub), priority 0 par défaut → +# cette page s'intercale ENTRE les deux : l'utilisateur clique « Commencer +# l'installation » sur le hub, voit CE récapitulatif (le disque n'a encore +# subi AUCUNE écriture), et l'installation ne démarre qu'à son clic +# « Lancer l'installation ». Le bouton « Précédent » ramène au hub pour +# modifier les choix (cf. _on_back_clicked : la file d'actions du GUI est +# consommée à chaque écran, on y réinsère le hub). +# +# CONTENU : configuration choisie par l'utilisateur (via les modules D-Bus +# d'Anaconda : langue, clavier, fuseau, comptes, partitionnement + les +# fichiers /tmp des spokes FWS : bureau, hostname) ET matériel détecté +# (virtualisation, GPU, disques, réseau) — mêmes sources que ce que le +# %post du kickstart et fws-setup-hardware appliqueront. +# +# ⚠ « completed » doit rester False : _instantiateAction SAUTE les standalone +# spokes complétées (mécanisme prévu pour les installs kickstart). +# ⚠ Chemin couplé à la version Python d'Arch (3.14) — comme fws_desktop.py — +# et le bouton « Précédent » s'appuie sur les internes du GUI d'anaconda +# 45.8 (_actions/_currentAction) : à re-vérifier à chaque bump d'anaconda. + +import inspect + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +from pyanaconda.anaconda_loggers import get_module_logger +from pyanaconda.core import util +from pyanaconda.modules.common.constants.objects import DISK_SELECTION +from pyanaconda.modules.common.constants.services import ( + LOCALIZATION, + STORAGE, + TIMEZONE, + USERS, +) +from pyanaconda.modules.common.structures.user import UserData +from pyanaconda.ui.gui import GraphicalUserInterface +from pyanaconda.ui.gui.hubs.summary import SummaryHub +from pyanaconda.ui.gui.spokes import StandaloneSpoke + +log = get_module_logger(__name__) + +__all__ = ["FWSRecapSpoke"] + +# Doit rester aligné sur le %post du kickstart (étape « Bureau choisi ») et +# sur les libellés de la spoke fws_desktop. +_DESKTOP_LABELS = { + "cli": "ligne de commande uniquement (aucun bureau)", + "gnome": "GNOME (+ Firefox, GDM)", + "kde": "KDE Plasma (+ Firefox, SDDM)", + "hyprland": "Hyprland (+ Firefox, SDDM)", + "xfce": "XFCE (+ Firefox, SDDM)", +} + +_PARTITIONING_LABELS = { + "AUTOMATIC": "automatique (recommandé)", + "INTERACTIVE": "personnalisé (assistant)", + "CUSTOM": "personnalisé", + "BLIVET": "avancé (blivet-gui)", + "MANUAL": "points de montage manuels", +} + + +def _safe(getter, default): + """Valeur d'un getter D-Bus/système, ou défaut : le récap ne doit JAMAIS + faire tomber l'installateur, quel que soit le module indisponible (d'où + le except large : dasbus lève des types variés selon la panne).""" + try: + return getter() + except Exception: # pylint: disable=broad-except + return default + + +def _capture(*argv): + """stdout d'une commande, chaîne vide si absente ou en échec.""" + try: + return util.execWithCapture(argv[0], list(argv[1:])) or "" + except (OSError, RuntimeError): + return "" + + +def _read_flag(path): + try: + with open(path) as fh: + return fh.read().strip() + except OSError: + return "" + + +def _config_lines(): + """Section « configuration choisie » : interroge les modules D-Bus + d'Anaconda (remplis par les spokes) + les fichiers /tmp des spokes FWS.""" + lines = [] + + lang = _safe(lambda: LOCALIZATION.get_proxy().Language, "") + if lang: + lines.append(f" • Langue : {lang}") + + layouts = _safe(lambda: list(LOCALIZATION.get_proxy().XLayouts), []) + if layouts: + lines.append(f" • Clavier (X11) : {', '.join(layouts)}") + + tz = _safe(lambda: TIMEZONE.get_proxy().Timezone, "") + if tz: + ntp = _safe(lambda: TIMEZONE.get_proxy().NTPEnabled, None) + suffix = {True: " — synchronisation NTP activée", + False: " — synchronisation NTP désactivée"}.get(ntp, "") + lines.append(f" • Fuseau horaire : {tz}{suffix}") + + users = _safe( + lambda: UserData.from_structure_list(USERS.get_proxy().Users), []) + if users: + names = ", ".join(u.name for u in users if u.name) + lines.append(f" • Utilisateur(s) : {names} (sudo via wheel)") + else: + lines.append(" • Utilisateur(s) : AUCUN — pensez-y avant de lancer !") + + root_locked = _safe(lambda: USERS.get_proxy().IsRootAccountLocked, None) + root_pw = _safe(lambda: USERS.get_proxy().IsRootPasswordSet, None) + if root_locked: + root_txt = "verrouillé (recommandé)" + elif root_pw: + root_txt = "actif, mot de passe défini" + elif root_pw is not None: + # Même règle que l'étape « Sécurité » du %post : un root resté sans + # mot de passe sera verrouillé en fin d'installation. + root_txt = "sans mot de passe → sera VERROUILLÉ en fin d'install" + else: + root_txt = "" + if root_txt: + lines.append(f" • Compte root : {root_txt}") + + # Hostname : écrit par le spoke Utilisateur (patch user.py du paquet + # anaconda). Même nettoyage / même défaut que le %post --nochroot. + hn = "".join(c for c in _read_flag("/tmp/fws-hostname") + if c.isalnum() or c in "._-") or "fws" + lines.append(f" • Nom de machine : {hn}") + + # Bureau : écrit par fws_desktop.apply() (sortie de la spoke). Absent = + # spoke jamais visitée = « cli », exactement le défaut du %post. + desk = _read_flag("/tmp/fws-desktop") or "cli" + lines.append(f" • Bureau : {_DESKTOP_LABELS.get(desk, desk)}") + + method = _safe( + lambda: STORAGE.get_proxy( + STORAGE.get_proxy().AppliedPartitioning).PartitioningMethod, "") + if method: + lines.append(" • Partitionnement : " + f"{_PARTITIONING_LABELS.get(method, method)}") + + return lines, desk, method + + +def _build_summary(): + config_lines, desk, method = _config_lines() + config_text = "\n".join(config_lines) \ + or " (configuration non disponible)" + + # Disques ciblés par l'installation (spoke « Destination »). + selected = set(_safe( + lambda: list(STORAGE.get_proxy(DISK_SELECTION).SelectedDisks), [])) + disks = [] + for line in _capture("lsblk", "-dno", "NAME,SIZE,MODEL,TYPE").splitlines(): + fields = line.split() + if fields and fields[-1] == "disk": + mark = " ← CIBLE de l'installation" if fields[0] in selected else "" + disks.append(" • " + " ".join(fields[:-1]) + mark) + disk_text = "\n".join(disks) or " (aucun disque détecté)" + + if method == "AUTOMATIC": + disk_warn = ( + " ⚠ Rien n'a encore été écrit. Le partitionnement automatique\n" + " EFFACERA ENTIÈREMENT le(s) disque(s) cible(s) au lancement.") + else: + disk_warn = ( + " ⚠ Rien n'a encore été écrit. Au lancement, ce sont vos choix\n" + " de partitionnement faits dans l'installateur qui s'appliqueront.") + + virt = (_capture("systemd-detect-virt").strip().splitlines() or ["none"])[0] + if virt in ("", "none"): + virt = "aucune (machine physique)" + + gpus = [] + for line in _capture("lspci").splitlines(): + low = line.lower() + if ("vga" in low or "3d" in low or "display" in low) and ":" in line: + gpus.append(" • " + line.split(":", 2)[-1].strip()) + gpu_text = "\n".join(gpus) or " (aucune carte graphique détectée)" + + try: + net_ok = util.execWithRedirect("ip", ["route", "get", "1.1.1.1"]) == 0 + except (OSError, RuntimeError): + net_ok = False + if net_ok: + net = "disponible ✔" + else: + net = ("INDISPONIBLE ✖ — bureau, drivers et stack FWS ne\n" + " s'installeront PAS (rattrapable au 1er boot)") + + extras = ("Steam, Wine, VR (Monado/OpenXR), winboat-bin" + if desk != "cli" else "rien de plus (choix CLI)") + + return f"""\ +══════════════════════════════════════════════════════════ + FWS — RÉCAPITULATIF AVANT INSTALLATION +══════════════════════════════════════════════════════════ + +CONFIGURATION CHOISIE +{config_text} + +DISQUES +{disk_warn} +{disk_text} + +MATÉRIEL DÉTECTÉ + • Virtualisation : {virt} + • Réseau : {net} + Cartes graphiques (drivers installés automatiquement en fin d'install) : +{gpu_text} + +DÉROULEMENT DE L'INSTALLATION + 1. Partitionnement, puis copie du système live FWS (payload LiveOS) + 2. Noyau + initramfs régénérés (mkinitcpio), GRUB installé + 3. Bureau choisi + navigateur installés (réseau requis) + 4. Matériel : drivers GPU, outils VM, son, Bluetooth, Podman + + si bureau choisi : {extras} + +« Précédent » pour revenir modifier vos choix — +« Lancer l'installation » pour démarrer réellement l'installation. +""" + + +class FWSRecapSpoke(StandaloneSpoke): + """Récapitulatif des choix, affiché entre le hub et l'installation.""" + + builderObjects = ["fwsRecapWindow"] + mainWidgetName = "fwsRecapWindow" + uiFile = "spokes/fws_recap.glade" + + postForHub = SummaryHub + priority = -10 + + @staticmethod + def get_screen_id(): + """Identifiant unique de cet écran.""" + return "fws-recap" + + @property + def completed(self): + # False, sinon _instantiateAction saute l'écran (cf. en-tête). + return False + + def apply(self): + # Rien à persister : cette page ne fait que montrer l'état. + pass + + def initialize(self): + StandaloneSpoke.initialize(self) + # Le bouton « continuer » de CETTE page est le vrai déclencheur : + # l'écran suivant (ProgressSpoke) démarre la file d'installation. + self.window.get_continue_button().set_label("_Lancer l'installation") + + # Capture pour « Précédent » : initialize() est appelé DEPUIS + # GraphicalUserInterface._on_continue_clicked (clic « Commencer + # l'installation » du hub), AVANT la bascule de _currentAction → à + # cet instant ui._currentAction est ENCORE l'instance du hub Résumé, + # et le « self » de l'appelant est l'UI. Aucune API publique ne les + # expose ; on remonte la pile d'appels (déterministe — pas de scan + # mémoire, pas de variable globale). + self._gui = None + self._hub = None + frame = inspect.currentframe() + try: + frame = frame.f_back + while frame is not None: + candidate = frame.f_locals.get("self") + if isinstance(candidate, GraphicalUserInterface): + self._gui = candidate + current = getattr(candidate, "_currentAction", None) + if isinstance(current, SummaryHub): + self._hub = current + break + frame = frame.f_back + finally: + del frame + + # Bouton « Précédent », placé avec les boutons de navigation + # (à côté de « Quitter » — leur parent est la GtkButtonBox du bas). + quit_button = self.window.get_quit_button() + button_box = quit_button.get_parent() + self._back_button = Gtk.Button.new_with_mnemonic("_Précédent") + self._back_button.connect("clicked", self._on_back_clicked) + button_box.add(self._back_button) + button_box.reorder_child(self._back_button, 0) + if self._gui is None or self._hub is None: + # Références introuvables (flux inattendu) : bouton grisé plutôt + # qu'une navigation cassée — l'install reste possible. + log.warning("FWS recap: hub/UI introuvables, « Précédent » désactivé.") + self._back_button.set_sensitive(False) + self._back_button.show() + + def _on_back_clicked(self, _button): + ui = self._gui + hub = self._hub + if ui is None or hub is None: + return + log.info("FWS recap: retour au hub Résumé pour modification.") + # La file d'actions du GUI est CONSOMMÉE (pop(0)) à chaque écran : + # ici _actions = [FWSRecapSpoke, ProgressSpoke] et le hub en est déjà + # sorti. On le réinsère en tête pour que le prochain « Commencer + # l'installation » ré-enchaîne hub → NOUVELLE page récap (résumé + # recalculé avec les choix modifiés) → installation. + if not ui._actions or ui._actions[0] is not SummaryHub: + ui._actions.insert(0, SummaryHub) + # _on_continue_clicked a désactivé le « continuer » du hub en le + # quittant : on le réarme, sinon « Commencer l'installation » est mort. + hub.window.set_may_continue(True) + # Même séquence que la transition officielle (_on_continue_clicked). + self.exited.emit(self) + hub.refresh() + ui.mainWindow.setCurrentAction(hub) + hub.entered.emit(hub) + ui._currentAction = hub + + def refresh(self): + StandaloneSpoke.refresh(self) + text = _build_summary() + self.builder.get_object("recapView").get_buffer().set_text(text) + # Copie pour le harnais de debug du .xinitrc (fws-debug.txt). + try: + with open("/tmp/fws-hw-summary.txt", "w") as fh: + fh.write(text) + except OSError: + pass