From 669a228286f2c5e50a3aca3c0daa2a530f0f7089 Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Sat, 22 Aug 2026 09:36:54 -0400 Subject: [PATCH] Install the desktop before anything allowed to fail, and let Terra be installed twice Two bugs, one story: ./install on a machine that had been half set up before produced no Hyprland at all, and said so in one line among twenty minutes of scrollback. Terra bootstraps itself with --repofrompath, which defines a throwaway repo id just long enough to install terra-release. Run it again on a machine that already has terra-release and dnf5 refuses the whole transaction -- the throwaway id collides with the real one. That step sits above everything, so set -e ended the stage before a single package was considered. It is skipped now when terra-release is already installed. The rest is the reason one failed repo cost the desktop. Hyprland was installed near the bottom of the stage, below a codec swap, two group updates and a GStreamer glob, any one of which can fail for reasons outside this repository. It now installs directly after the packages it needs and before anything optional, and everything fragile below it runs through a soft helper that logs and continues rather than ending the run. What was stepped over is listed at the end, because tolerating a failure only beats aborting on it if somebody is told. A missing Hyprland is still fatal, and now says so in words. Also removes the leftover disabled solopasha/hyprland COPR, which would mix with lionheartp's the moment anyone enabled it while debugging. Fixes the usage widget reading 1500%: the endpoint reports percentages, not 0..1 fractions. Clamped as well, and the widget answers a click now -- it set interactive:false, which disables the mouse area its own handler needed. --- README.md | 2 +- .../modules/bar/AgentUsageWidget.qml | 57 ++++--- .../dot/quickshell/scripts/panama-agent-usage | 7 +- migrations/1787672400.sh | 41 +++++ setup/scripts/install-packages | 154 +++++++++++++----- tests/quickshell/agent-usage-contract | 36 +++- tests/setup/desktop-first-contract | 110 +++++++++++++ 7 files changed, 340 insertions(+), 67 deletions(-) create mode 100755 migrations/1787672400.sh create mode 100755 tests/setup/desktop-first-contract diff --git a/README.md b/README.md index 4c899e5..4b241d0 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ docs/ Settings reference, and the design specs behind the work ## Tests -152 of them, under `tests/`. Run the lot, or a subset by pattern: +153 of them, under `tests/`. Run the lot, or a subset by pattern: ```sh panama test # everything diff --git a/config/dot/quickshell/modules/bar/AgentUsageWidget.qml b/config/dot/quickshell/modules/bar/AgentUsageWidget.qml index 6d9ce0a..c41c8f4 100644 --- a/config/dot/quickshell/modules/bar/AgentUsageWidget.qml +++ b/config/dot/quickshell/modules/bar/AgentUsageWidget.qml @@ -8,9 +8,14 @@ // it is a coding-tool readout, not something a general-purpose desktop shows // without being asked. // -// Children go straight into Pill's own Row -- it adopts them through its -// default alias, so wrapping them in another Row and centring that is both -// redundant and a warning at load. +// The glyph and the number live in their own Row, the same shape VitalsField +// uses. Anchoring them straight into Pill's layout Row instead made each child +// centre against a Row whose height those same children determine, and they +// came out sitting off-centre against the rest of the bar. +// +// Clickable, because a readout you cannot ask anything of is furniture. Left +// click opens the settings that govern it; hovering says which window the +// number belongs to and when it resets. import QtQuick import qs.config @@ -21,28 +26,36 @@ Pill { id: root visible: Settings.showAgentUsage && AgentUsage.available - interactive: false + onActivated: ShellState.openSettings("appearance") onSecondaryActivated: ShellState.openSettings("appearance") - Text { - anchors.verticalCenter: parent.verticalCenter - text: "\u{F1719}" // md-robot-outline - color: { - if (AgentUsage.headline >= 90) return Theme.danger; - if (AgentUsage.headline >= 75) return Theme.warn; - return Theme.fgDim; - } - font.family: Theme.fontMono - font.pixelSize: Theme.fontSizeSmall - } + Row { + spacing: 4 - Text { - anchors.verticalCenter: parent.verticalCenter - text: AgentUsage.headline + "%" - color: Theme.fg - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall - font.features: Theme.tabularFigures + Text { + anchors.verticalCenter: parent.verticalCenter + text: "\u{F1719}" // md-robot-outline + color: { + if (AgentUsage.headline >= 90) return Theme.danger; + if (AgentUsage.headline >= 75) return Theme.warn; + return Theme.fgDim; + } + font.family: Theme.fontMono + font.pixelSize: Theme.fontSize + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: AgentUsage.headline + "%" + color: Theme.fg + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + font.features: Theme.tabularFigures + // Fixed and right-aligned so the bar does not shuffle sideways when + // the number gains a digit, exactly as VitalsField does. + horizontalAlignment: Text.AlignRight + width: 30 + } } } diff --git a/config/dot/quickshell/scripts/panama-agent-usage b/config/dot/quickshell/scripts/panama-agent-usage index 5536dd4..a3e4127 100755 --- a/config/dot/quickshell/scripts/panama-agent-usage +++ b/config/dot/quickshell/scripts/panama-agent-usage @@ -95,7 +95,12 @@ fi # field is optional: an endpoint that stops reporting one should cost that # number, not the whole indicator. usage="$(jq -c ' - def pct: if type == "number" then (. * 100 | round) else null end; + # The endpoint reports utilisation as a percentage already -- 15 means 15%. + # This multiplied by 100 on the assumption it was a 0..1 fraction, which is + # how the bar came to read 1500%. Clamped as well as rounded, because a + # readout is a number you glance at and trust; one that can exceed 100 + # teaches you not to. + def pct: if type == "number" then ([[(. | round), 0] | max, 100] | min) else null end; { tier: (.rate_limit_tier // .rateLimitTier // null), subscription: (.subscription_type // .subscriptionType // null), diff --git a/migrations/1787672400.sh b/migrations/1787672400.sh new file mode 100755 index 0000000..c34e5ff --- /dev/null +++ b/migrations/1787672400.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# Remove the solopasha/hyprland COPR left over from an earlier setup. +# +# Panama installs Hyprland from lionheartp/Hyprland. A machine set up before +# that decision -- or half set up from sunhat-era notes -- can still carry +# solopasha's COPR repo file. It arrives disabled, so nothing is broken today, +# and that is exactly why it is worth removing now rather than later: the moment +# somebody enables it while debugging, two repositories provide hyprland, +# hyprland-uwsm and xdg-desktop-portal-hyprland, and dnf resolves the split by +# version rather than by intent. Half the desktop from one COPR and half from +# the other is a very bad afternoon. +# +# Only Panama's business if it is disabled. A machine that has deliberately +# enabled solopasha is a machine somebody made a choice about, and a migration +# is not the place to overrule it. + +set -euo pipefail + +repo_file="/etc/yum.repos.d/_copr:copr.fedorainfracloud.org:solopasha:hyprland.repo" + +[[ -e "$repo_file" ]] || exit 0 + +# Enabled means somebody chose it. Say so and leave. +if grep -qE '^\s*enabled\s*=\s*1' "$repo_file"; then + echo "Leaving solopasha/hyprland alone: it is enabled, so it is in use deliberately." + echo "Panama installs Hyprland from lionheartp/Hyprland; having both enabled will" + echo "eventually mix the two. Disable it with: sudo dnf copr disable solopasha/hyprland" + exit 0 +fi + +PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}" +sudo_cmd=(sudo) +[[ -x "$PANAMA_PATH/bin/panama-sudo" ]] && sudo_cmd=( + "$PANAMA_PATH/bin/panama-sudo" --reason + "Removing the unused solopasha/hyprland repository, which conflicts with the one Panama installs from" + -- +) + +"${sudo_cmd[@]}" rm -f "$repo_file" +echo "Removed the disabled solopasha/hyprland COPR; Hyprland comes from lionheartp/Hyprland." diff --git a/setup/scripts/install-packages b/setup/scripts/install-packages index a3410c5..e820a7d 100755 --- a/setup/scripts/install-packages +++ b/setup/scripts/install-packages @@ -22,6 +22,23 @@ packages_in() { sed 's/#.*//' "$1" | tr "\n" " " } +# Runs something whose failure must not cost you the desktop. +# +# `set -e` above is right for the packages Panama cannot work without and wrong +# for everything else. A codec swap that finds nothing to swap, a group update +# renamed upstream, a third-party host that is down -- each of those used to end +# this stage wherever it happened to sit, and the desktop was installed near the +# bottom, so any one of them meant a machine with no Hyprland on it and a single +# line of dnf output to explain why. +# +# So the ordering rule for this file: anything that can fail for a reason +# outside this repository goes below the desktop, and goes through here. +soft() { + local what="$1"; shift + "$@" >/dev/null 2>&1 || { log "$what did not complete; continuing"; softly_failed+=("$what"); } +} +softly_failed=() + # --- Defined Paths --- # The default, not an assignment: ./install and link-dotfiles honor an exported # PANAMA_PATH, and clobbering it here made a clone anywhere else source the @@ -41,32 +58,27 @@ sudo dnf config-manager setopt fedora-cisco-openh264.enabled=1 log "Installing RPM Fusion AppStream Metadata" sudo dnf update @core -y > /dev/null sudo dnf install -y rpmfusion-\*-appstream-data > /dev/null -log "Installing Terra Repository" -sudo dnf install -y --nogpgcheck --repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' terra-release > /dev/null +# Terra bootstraps itself: --repofrompath defines a throwaway repo just long +# enough to install terra-release, which then writes the real /etc/yum.repos.d +# entry. Doing that a second time is not harmless -- dnf5 refuses the whole +# transaction with 'Id is present more than once in the configuration', because +# the throwaway id collides with the one terra-release already installed. +# +# That is what killed a re-run on a machine Terra had already reached: this sits +# in the repository section, above everything, so `set -e` ended the stage +# before a single package was considered. An installer whose second run does +# less than its first is worse than one that never ran. +if rpm -q terra-release >/dev/null 2>&1; then + log "Terra repository already installed" +else + log "Installing Terra Repository" + sudo dnf install -y --nogpgcheck --repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' terra-release > /dev/null +fi echo -e "\n--- Installing relevant packages ---" log "Updating all packages. This may take a while" sudo dnf update -y --refresh > /dev/null -log "Updating core, multimedia, and sound-and-video groups" -# A trailing `&& sync` here previously meant a failing groupupdate was exempt -# from set -e (bash does not apply -e to the left side of a && list), so the -# failure went unreported. Sync unconditionally on its own line instead. -sudo dnf4 groupupdate -y 'core' 'multimedia' 'sound-and-video' \ - --setop='install_weak_deps=False' \ - --exclude='PackageKit-gstreamer-plugin' \ - --allowerasing > /dev/null -sync -log "Swapping ffmpeg-free for ffmpeg" -sudo dnf swap -y 'ffmpeg-free' 'ffmpeg' --allowerasing > /dev/null -log "Swapping mesa-va-drivers for mesa-va-drivers-freeworld" -sudo dnf swap -y mesa-va-drivers mesa-va-drivers-freeworld > /dev/null -log "Upgrading Multimedia group with optional packages" -sudo dnf4 group upgrade -y --with-optional Multimedia > /dev/null -log "Installing GStreamer plugins (bad, good, base)" -sudo dnf install -y gstreamer1-plugins-{bad-\*,good-\*,base} \ - --exclude=gstreamer1-plugins-bad-free-devel > /dev/null - # --- Install all initial packages --- PACKAGES_FILE="$PANAMA_PATH/setup/packages/initial-packages" if [[ -f "$PACKAGES_FILE" ]]; then @@ -93,6 +105,68 @@ else log "Package list was not in specified path: $DESKTOP_FILE" fi +# --- Install the Hyprland desktop --- +# +# Directly after desktop-packages, which is what supplies the dnf plugin that +# `dnf copr` needs, and deliberately before anything optional. This is the one +# thing on the list that Panama is; a machine that gets only this far is a +# machine you can log into, and every step below it is a convenience. +# +# Most of these live in the lionheartp/Hyprland COPR rather than Fedora proper. +HYPR_FILE="$PANAMA_PATH/setup/packages/hyprland-packages" +if [[ -f "$HYPR_FILE" ]]; then + log "Enabling Hyprland COPR" + sudo dnf copr enable -y lionheartp/Hyprland > /dev/null + HYPR_PACKAGES=$(packages_in "$HYPR_FILE") + log "Installing Hyprland desktop packages" + echo -e "Includes the following packages:" + echo -e "$(<"$HYPR_FILE")" + sudo dnf install -y --setopt=install_weak_deps=False $HYPR_PACKAGES > /dev/null + log "Hyprland packages installed!" +else + log "Package list was not in specified path: $HYPR_FILE" +fi + +# Said out loud, because the failure this guards against was silent. The stage +# used to die somewhere above this point and report one red line among twenty +# minutes of scrollback, and the machine looked installed until you tried to log +# into it. +if rpm -q hyprland >/dev/null 2>&1; then + log "Hyprland $(rpm -q --queryformat '%{VERSION}' hyprland) is installed." +else + log "Hyprland is NOT installed. Nothing below this point will give you a desktop." + exit 1 +fi + +# --- Codecs and multimedia --------------------------------------------------- +# +# Below the desktop and every one of them non-fatal, because none is a +# dependency of it and each can fail for reasons that have nothing to do with +# this repository -- a swap whose source package this spin never shipped, a +# group renamed upstream between Fedora releases. +# +# A trailing `&& sync` on the group update previously meant a failure was exempt +# from set -e as well (bash does not apply -e to the left of a && list), so it +# went unreported rather than being deliberately tolerated. It is deliberate now. + +log "Updating core, multimedia, and sound-and-video groups" +soft "the multimedia group update" \ + sudo dnf4 groupupdate -y 'core' 'multimedia' 'sound-and-video' \ + --setop='install_weak_deps=False' \ + --exclude='PackageKit-gstreamer-plugin' \ + --allowerasing +sync +log "Swapping ffmpeg-free for ffmpeg" +soft "the ffmpeg swap" sudo dnf swap -y 'ffmpeg-free' 'ffmpeg' --allowerasing +log "Swapping mesa-va-drivers for mesa-va-drivers-freeworld" +soft "the mesa driver swap" sudo dnf swap -y mesa-va-drivers mesa-va-drivers-freeworld +log "Upgrading Multimedia group with optional packages" +soft "the optional Multimedia upgrade" sudo dnf4 group upgrade -y --with-optional Multimedia +log "Installing GStreamer plugins (bad, good, base)" +soft "the GStreamer plugins" \ + sudo dnf install -y gstreamer1-plugins-{bad-\*,good-\*,base} \ + --exclude=gstreamer1-plugins-bad-free-devel + # --- Install Development Packages needed for Neovim --- DEV_FILE="$PANAMA_PATH/setup/packages/development-packages" if [[ -f "$DEV_FILE" ]]; then @@ -100,7 +174,7 @@ if [[ -f "$DEV_FILE" ]]; then log "Installing Development Packages. Mostly for Neovim." echo -e "Includes the following packages:" echo -e "$(<"$DEV_FILE")" - sudo dnf install -y $DEV_PACKAGES > /dev/null + soft "the development packages" sudo dnf install -y $DEV_PACKAGES log "Development packages installed!" else log "Package list was not in specified path: $DEV_FILE" @@ -136,22 +210,6 @@ else log "nvm is not installed, so Node was not set up" fi -# --- Install the Hyprland desktop --- -# Most of these live in the lionheartp/Hyprland COPR rather than Fedora proper. -HYPR_FILE="$PANAMA_PATH/setup/packages/hyprland-packages" -if [[ -f "$HYPR_FILE" ]]; then - log "Enabling Hyprland COPR" - sudo dnf copr enable -y lionheartp/Hyprland > /dev/null - HYPR_PACKAGES=$(packages_in "$HYPR_FILE") - log "Installing Hyprland desktop packages" - echo -e "Includes the following packages:" - echo -e "$(<"$HYPR_FILE")" - sudo dnf install -y --setopt=install_weak_deps=False $HYPR_PACKAGES > /dev/null - log "Hyprland packages installed!" -else - log "Package list was not in specified path: $HYPR_FILE" -fi - # --- Applications no repository packages ------------------------------------- # # Everything else Panama installs comes from dnf or Flathub. These four do not @@ -238,11 +296,14 @@ FLATPAK_FILE="$PANAMA_PATH/setup/packages/flatpak-packages" if [[ -f "$FLATPAK_FILE" ]]; then FLATPAK_PACKAGES=$(packages_in "$FLATPAK_FILE") log "Adding Flathub remote" - sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo > /dev/null + soft "adding the Flathub remote" \ + sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo log "Installing Flatpak Packages" echo -e "Includes the following packages:" echo -e "$(<"$FLATPAK_FILE")" - sudo flatpak install -y flathub $FLATPAK_PACKAGES > /dev/null + # One ID renamed on Flathub must not cost the rest of the run; the desktop + # is already installed by this point and none of these is part of it. + soft "some Flatpak packages" sudo flatpak install -y flathub $FLATPAK_PACKAGES log "Flatpak packages installed!" else log "Package list was not in specified path: $FLATPAK_FILE" @@ -301,3 +362,16 @@ for extra in ${PANAMA_EXTRAS:-}; do log "No such extras category: $extra" fi done + +# --- What was stepped over --------------------------------------------------- +# +# Tolerating a failure is only better than aborting on it if somebody is told. +# This stage now survives a codec swap that finds nothing to swap, and the whole +# point of surviving it is that the desktop gets installed anyway -- but a +# machine missing its video codecs should say so once, here, rather than be +# discovered a week later by a video that will not play. +if (( ${#softly_failed[@]} > 0 )); then + log "Installed, but these were stepped over:" + printf ' - %s\n' "${softly_failed[@]}" + log "None of them stops the desktop. Re-run this stage to try them again." +fi diff --git a/tests/quickshell/agent-usage-contract b/tests/quickshell/agent-usage-contract index e680982..588053b 100755 --- a/tests/quickshell/agent-usage-contract +++ b/tests/quickshell/agent-usage-contract @@ -54,7 +54,7 @@ CREDS cat >"$stub/curl" <>"$work/curl-argv" -printf '%s\n' '{"five_hour":{"utilization":0.42,"resets_at":"2026-08-22T14:00:00Z"},"seven_day":{"utilization":0.71,"resets_at":"2026-08-27T00:00:00Z"},"rate_limit_tier":"default_claude_max_5x"}' +printf '%s\n' '{"five_hour":{"utilization":42,"resets_at":"2026-08-22T14:00:00Z"},"seven_day":{"utilization":71,"resets_at":"2026-08-27T00:00:00Z"},"rate_limit_tier":"default_claude_max_5x"}' STUB chmod +x "$stub/curl" @@ -108,10 +108,40 @@ uncommented "$collector" | grep -qE '>[[:space:]]*"?\$?\{?CREDENTIALS' \ # ── The reading it produced ───────────────────────────────────────────────── +# The endpoint already reports percentages. Treating them as 0..1 fractions and +# multiplying is how the bar came to read 1500%. [[ "$(jq -r '.usage.fiveHour.used' "$output")" == "42" ]] \ - || note 'the five-hour utilisation was not converted to a percentage' + || note "the five-hour reading is $(jq -r '.usage.fiveHour.used' "$output") for a reported 42%" [[ "$(jq -r '.usage.week.used' "$output")" == "71" ]] \ - || note 'the weekly utilisation was not converted to a percentage' + || note "the weekly reading is $(jq -r '.usage.week.used' "$output") for a reported 71%" + +# Nothing the widget shows may fall outside the range a percentage has, whatever +# the endpoint says. A readout that can print 1500% is one you learn to ignore. +cat >"$stub/curl" <<'STUB' +#!/usr/bin/env bash +printf '%s\n' '{"five_hour":{"utilization":1500},"seven_day":{"utilization":-4}}' +STUB +chmod +x "$stub/curl" +run +[[ "$(jq -r '.usage.fiveHour.used' "$output")" == "100" ]] \ + || note 'an out-of-range reading was not clamped to 100' +[[ "$(jq -r '.usage.week.used' "$output")" == "0" ]] \ + || note 'a negative reading was not clamped to 0' + +cat >"$stub/curl" <<'STUB' +#!/usr/bin/env bash +printf '%s\n' '{"five_hour":{"utilization":42,"resets_at":"2026-08-22T14:00:00Z"},"seven_day":{"utilization":71,"resets_at":"2026-08-27T00:00:00Z"},"rate_limit_tier":"default_claude_max_5x"}' +STUB +chmod +x "$stub/curl" + +# ── It answers a click ────────────────────────────────────────────────────── +# +# Pill's MouseArea is gated on `interactive`, so a widget that sets it false and +# connects onSecondaryActivated has a handler nothing can ever reach. +uncommented "$widget" | grep -q 'interactive: false' \ + && note 'the widget disables Pill''s mouse area, so its click handlers never fire' +uncommented "$widget" | grep -q 'onActivated' \ + || note 'left-clicking the widget does nothing' # An endpoint that answers with something else must degrade, not crash. cat >"$stub/curl" <<'STUB' diff --git a/tests/setup/desktop-first-contract b/tests/setup/desktop-first-contract new file mode 100755 index 0000000..a8b06d8 --- /dev/null +++ b/tests/setup/desktop-first-contract @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +# The desktop installs before anything that is allowed to fail. +# +# install-packages runs under `set -euo pipefail`, and the Hyprland block used +# to sit near the bottom of it, below a codec swap, two group updates and a +# GStreamer glob. Any one of those exiting non-zero ended the stage where it +# stood. On a machine whose spin never shipped ffmpeg-free, the swap failed, the +# stage died, and the install finished reporting one red line among twenty +# minutes of scrollback -- with no Hyprland on the disk at all. +# +# The rule that prevents it: anything that can fail for a reason outside this +# repository goes BELOW the desktop, and goes through `soft`. What is left above +# the desktop is only what the desktop needs. +# +# This pins the order, not the individual commands, because the next fragile +# thing somebody adds will not be a codec swap. + +set -uo pipefail + +repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +installer="$repo_dir/setup/scripts/install-packages" + +findings=() +note() { findings+=("$1"); } + +[[ -x "$installer" ]] || { printf 'desktop first contract: %s is not executable\n' "$installer" >&2; exit 1; } + +line_of() { grep -n "$1" "$installer" | head -1 | cut -d: -f1; } + +hyprland_at="$(line_of '^HYPR_FILE=')" +[[ -n "$hyprland_at" ]] || { printf 'desktop first contract: no Hyprland block found\n' >&2; exit 1; } + +# ── Nothing fragile above the desktop ─────────────────────────────────────── +# +# Named individually rather than by pattern: each is a command whose failure is +# survivable, and each one above the Hyprland block is a machine that boots to +# nothing. + +for fragile in 'dnf swap' 'groupupdate' 'group upgrade' 'gstreamer1-plugins' \ + 'flatpak install' 'nvm install' 'curl -fsSL'; do + while read -r at; do + (( at < hyprland_at )) \ + && note "'$fragile' runs at line $at, above the desktop at line $hyprland_at" + done < <(grep -n -- "$fragile" "$installer" | grep -v '^\s*#' | cut -d: -f1) +done + +# ── Everything fragile is actually tolerated ──────────────────────────────── +# +# Being below the desktop is only half of it. A bare `dnf swap` below the +# Hyprland block still kills every step after it -- the flatpaks, the extras +# somebody explicitly chose. + +# Comments dropped and backslash continuations joined, so a `soft` invocation +# wrapped across three lines reads as the one command it is. +uncommented() { grep -vE '^\s*#' "$installer" | sed -e :a -e '/\\$/N; s/\\\n\s*/ /; ta'; } + +while read -r command; do + uncommented | grep -q "soft .*$command" \ + || note "'$command' runs without soft, so its failure still ends the stage" +done <<'FRAGILE' +dnf swap -y 'ffmpeg-free' +dnf swap -y mesa-va-drivers +groupupdate +group upgrade +gstreamer1-plugins +flatpak install -y flathub $FLATPAK_PACKAGES +FRAGILE + +grep -q '^soft()' "$installer" \ + || note 'install-packages defines no soft helper, so nothing can be tolerated deliberately' + +# ── The desktop failing is still fatal, and still said out loud ───────────── +# +# The inverse mistake: making everything survivable turns a machine with no +# desktop into a run that reports success. + +python3 - "$installer" "$hyprland_at" <<'PY' || note 'a missing Hyprland does not stop the stage' +import sys +lines = open(sys.argv[1], encoding="utf-8").read().splitlines() +start = int(sys.argv[2]) +after = "\n".join(lines[start:start + 40]) +if "rpm -q hyprland" not in after or "exit 1" not in after: + raise SystemExit(1) +PY + +uncommented | grep -q 'soft .*HYPR_PACKAGES' \ + && note 'the Hyprland install is tolerated, so a machine with no desktop reports success' + +# ── Soft failures are reported ────────────────────────────────────────────── + +grep -q 'softly_failed' "$installer" \ + || note 'nothing collects what was stepped over, so a tolerated failure is a silent one' + +python3 - "$installer" <<'PY' || note 'the list of stepped-over steps is never printed' +import sys +text = open(sys.argv[1], encoding="utf-8").read() +tail = text[text.rindex("softly_failed"):] +if "printf" not in tail and "log" not in tail: + raise SystemExit(1) +PY + +if (( ${#findings[@]} > 0 )); then + mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u) + printf 'desktop first contract: %d finding(s)\n' "${#findings[@]}" >&2 + printf ' - %s\n' "${findings[@]}" >&2 + exit 1 +fi + +printf 'desktop first contract: PASS (desktop at line %s, everything fragile below it)\n' "$hyprland_at"