#!/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 that EXECUTES above the Hyprland block is a machine # that boots to nothing. # # Executes, not appears: the fragile steps live in functions defined near the # top (the server role calls them without ever reaching a desktop section), # and a definition runs nothing. So function bodies are excluded from the # position scan, and the desktop path's calls to those functions are required # to sit below the desktop instead. while IFS= read -r finding; do [[ -n "$finding" ]] && note "$finding" done < <(python3 - "$installer" "$hyprland_at" <<'PY' import re, sys path, hypr = sys.argv[1], int(sys.argv[2]) lines = open(path, encoding="utf-8").read().splitlines() in_body = False body = set() for i, line in enumerate(lines, 1): if not in_body and re.match(r'^[a-z_]+\(\)\s*\{', line): in_body = True body.add(i) continue if in_body: body.add(i) if line == '}': in_body = False fragile = ['dnf swap', 'groupupdate', 'group upgrade', 'gstreamer1-plugins', 'flatpak install', 'nvm install', 'curl -fsSL'] for i, line in enumerate(lines, 1): if i >= hypr or i in body or line.strip().startswith('#'): continue for needle in fragile: if needle in line: print(f"'{needle}' runs at line {i}, above the desktop at line {hypr}") # The desktop path still has to run the fragile helpers -- below the desktop. # (The server path calls them above, inside a branch that exits before the # desktop section; the exit is asserted back in bash.) for call in ('setup_node', 'install_bun', 'install_claude_code', 'install_codex'): calls = [i for i, line in enumerate(lines, 1) if re.match(r'^\s*' + call + r'\s*$', line) and i not in body] if not calls: print(f"{call} is never called, so the desktop path skips it") elif not any(i > hypr for i in calls): print(f"{call} is only called above the desktop") PY ) # The server branch is what excuses fragile calls above the desktop, and only # because it never falls through into the desktop section. sed -n '/^if \[\[ "\$ROLE" == server \]\]; then/,/^fi/p' "$installer" | grep -q '^\s*exit 0' \ || note 'the server branch does not exit before the desktop section' # ── 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"