diff --git a/bin/panama b/bin/panama index aabdf61..e5774e6 100755 --- a/bin/panama +++ b/bin/panama @@ -10,6 +10,7 @@ # doctor Report what is actually running on this machine # test Run every contract under tests/ # upgrade Re-run the installer from anywhere +# apps Choose applications to install, by category # app Build and install an application that no repository packages # help Show this help # @@ -72,6 +73,9 @@ ${BOLD}Commands:${RESET} a subset: 'panama test dock' runs the ones matching 'dock'. ${GREEN}upgrade${RESET} Re-run ./install from anywhere. Safe: every stage is idempotent and this is the documented upgrade path. + ${GREEN}apps${RESET} Choose applications to install: pick a category, then tick + what you want. The same catalog ./install offers, minus the + install. ${GREEN}app${RESET} Build and install an application that neither dnf nor Flathub carries. With no name, lists what is available. ${GREEN}help${RESET} Show this help (also -h, --help). @@ -86,6 +90,7 @@ ${BOLD}Examples:${RESET} $PROGRAM doctor --summary $PROGRAM test dock $PROGRAM upgrade + $PROGRAM apps $PROGRAM app $PROGRAM app claude-desktop EOF @@ -393,6 +398,119 @@ cmd_app() { fi } +# ---------------------------------------------------------------------------- +# Command: apps +# ---------------------------------------------------------------------------- +# The optional applications, chosen a few at a time rather than all at once. +# +# ./install offers the same catalog as whole categories, because during a first +# install you want coarse and fast. This is the other end of it: pick a +# category, then tick the applications inside, and install just those. Both read +# setup/lib/extras-catalog, so neither can drift from the other. +# +# Source-built applications appear here too, as their own category. They are not +# part of ./install for good reason -- a build is slow, wants the network +# throughout, and can fail on an upstream that moved -- but "choose, then +# install" is exactly the right shape for them, which is what this is. +cmd_apps() { + if ! command -v gum >/dev/null 2>&1; then + err "gum is not installed. It is in setup/packages/initial-packages; run 'panama upgrade'." + exit 1 + fi + + # shellcheck source=../setup/lib/extras-catalog + source "$PANAMA_DIR/setup/lib/extras-catalog" + + local extras_dir="$PANAMA_DIR/setup/packages/extras" + local apps_dir="$PANAMA_DIR/setup/apps" + + local -a categories=() + mapfile -t categories < <(catalog_categories "$extras_dir") + [[ -d "$apps_dir" ]] && categories+=("built from source") + + (( ${#categories[@]} > 0 )) || { err "No application categories found."; exit 1; } + + local category + category="$(gum choose --header "Which kind of application?" "${categories[@]}")" || return 0 + [[ -n "$category" ]] || return 0 + + if [[ "$category" == "built from source" ]]; then + cmd_apps_source "$apps_dir" + return + fi + + local file="$extras_dir/$category" + local -a labels=() targets=() + local target label marker + while IFS=$'\t' read -r target label; do + [[ -n "$target" ]] || continue + # Marked, not hidden: reinstalling what is present is harmless, but a menu + # that silently omits it leaves you wondering where it went. + if catalog_installed "$target"; then marker=" (installed)"; else marker=""; fi + targets+=("$target") + labels+=("$label$marker") + done < <(catalog_entries "$file") + + local chosen + chosen="$(gum choose --no-limit --header "$category — space to select, enter to accept" "${labels[@]}")" || return 0 + [[ -n "$chosen" ]] || { info "Nothing selected."; return 0; } + + # Back from labels to install targets, then out to everything each one carries. + local -a install_targets=() + local pick i + while IFS= read -r pick; do + [[ -n "$pick" ]] || continue + for i in "${!labels[@]}"; do + if [[ "${labels[$i]}" == "$pick" ]]; then + mapfile -t -O "${#install_targets[@]}" install_targets < <(catalog_targets "$file" "${targets[$i]}") + break + fi + done + done <<< "$chosen" + + local -a dnf_packages=() flatpak_ids=() + for target in "${install_targets[@]}"; do + if [[ "$target" == flatpak:* ]]; then flatpak_ids+=("${target#flatpak:}"); else dnf_packages+=("$target"); fi + done + + header "About to install" + (( ${#dnf_packages[@]} > 0 )) && printf ' dnf %s\n' "${dnf_packages[*]}" + (( ${#flatpak_ids[@]} > 0 )) && printf ' flatpak %s\n' "${flatpak_ids[*]}" + echo + confirm "Install these?" || { warn "Nothing installed."; return 0; } + + if (( ${#dnf_packages[@]} > 0 )); then + info "Installing ${#dnf_packages[@]} package(s) with dnf" + sudo dnf install -y "${dnf_packages[@]}" || warn "Some packages did not install" + fi + if (( ${#flatpak_ids[@]} > 0 )); then + info "Installing ${#flatpak_ids[@]} flatpak(s)" + sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo >/dev/null + sudo flatpak install -y flathub "${flatpak_ids[@]}" || warn "Some flatpaks did not install" + fi + ok "Done." +} + +# The source-built applications, offered by the same two-step flow and handed to +# the existing `panama app` so there is one build path, not two. +cmd_apps_source() { + local apps_dir="$1" + local -a names=() + mapfile -t names < <(for f in "$apps_dir"/*; do [[ -f "$f" ]] && basename "$f"; done) + (( ${#names[@]} > 0 )) || { err "No applications in $apps_dir."; return 1; } + + local chosen + chosen="$(gum choose --no-limit --header "Built from source — these take a while" "${names[@]}")" || return 0 + [[ -n "$chosen" ]] || { info "Nothing selected."; return 0; } + + local name + while IFS= read -r name; do + [[ -n "$name" ]] || continue + header "Building $name" + cmd_app "$name" || warn "$name did not build" + done <<< "$chosen" +} + # ---------------------------------------------------------------------------- # Dispatcher # ---------------------------------------------------------------------------- @@ -405,6 +523,7 @@ main() { test) shift; cmd_test "$@" ;; upgrade) shift; cmd_upgrade "$@" ;; app) shift; cmd_app "$@" ;; + apps) shift; cmd_apps "$@" ;; help|-h|--help|"") usage ;; --version) printf '%s %s\n' "$PROGRAM" "$VERSION" ;; *) diff --git a/setup/lib/extras-catalog b/setup/lib/extras-catalog new file mode 100755 index 0000000..aa553f3 --- /dev/null +++ b/setup/lib/extras-catalog @@ -0,0 +1,107 @@ +#!/usr/bin/env bash + +# Reading the optional-application catalog. +# +# Sourced by both front doors -- the interview's checklist during ./install, and +# `panama apps` afterwards -- so the two can never disagree about what exists. +# One catalog, parsed once, here. +# +# The format is one file per category under setup/packages/extras/, and a line +# is one of: +# +# gimp a dnf package +# flatpak:org.gimp.GIMP a Flathub id +# flatpak:com.slack.Slack | Slack either, with a name for the menu +# flatpak:com.obsproject.Studio.Plugin.SceneSwitcher +# +# An indented line belongs to the entry above it and is installed with it, which +# is how OBS carries its sixteen plugin extensions as a single thing to tick +# rather than seventeen. Comments and blank lines are ignored. +# +# Without an explicit name, the menu derives one from the last dotted component +# of a Flathub id, or uses the package name as it stands. + +# Every category name, one per line. +catalog_categories() { + local dir="$1" file + for file in "$dir"/*; do + [[ -f "$file" ]] && basename "$file" + done +} + +# The selectable entries in one category, as `targetlabel`. Indented +# continuation lines are folded into the entry above and never listed. +catalog_entries() { + local file="$1" line target label + while IFS= read -r line; do + line="${line%%#*}" + [[ "$line" =~ ^[[:space:]]*$ ]] && continue + [[ "$line" =~ ^[[:space:]] ]] && continue + + if [[ "$line" == *"|"* ]]; then + target="${line%%|*}" + label="${line#*|}" + else + target="$line" + label="" + fi + target="${target#"${target%%[![:space:]]*}"}"; target="${target%"${target##*[![:space:]]}"}" + label="${label#"${label%%[![:space:]]*}"}"; label="${label%"${label##*[![:space:]]}"}" + + if [[ -z "$label" ]]; then + label="${target#flatpak:}" + [[ "$target" == flatpak:* ]] && label="${label##*.}" + fi + printf '%s\t%s\n' "$target" "$label" + done <"$file" +} + +# Every install target for one entry: the entry itself, then the indented lines +# beneath it. Called with the category file and the entry's target. +catalog_targets() { + local file="$1" want="$2" line target seen=0 + while IFS= read -r line; do + line="${line%%#*}" + [[ "$line" =~ ^[[:space:]]*$ ]] && continue + + if [[ "$line" =~ ^[[:space:]] ]]; then + (( seen == 1 )) || continue + line="${line#"${line%%[![:space:]]*}"}" + printf '%s\n' "${line%%|*}" | sed 's/[[:space:]]*$//' + continue + fi + + target="${line%%|*}" + target="${target#"${target%%[![:space:]]*}"}"; target="${target%"${target##*[![:space:]]}"}" + if [[ "$target" == "$want" ]]; then + seen=1 + printf '%s\n' "$target" + elif (( seen == 1 )); then + break + fi + done <"$file" +} + +# Every install target in a whole category, in file order. +catalog_all_targets() { + local file="$1" line + while IFS= read -r line; do + line="${line%%#*}" + [[ "$line" =~ ^[[:space:]]*$ ]] && continue + line="${line#"${line%%[![:space:]]*}"}" + line="${line%%|*}" + printf '%s\n' "${line%"${line##*[![:space:]]}"}" + done <"$file" +} + +# Is this target already on the machine? Used to mark the menu, never to skip +# silently -- a re-run that reinstalls what is present is harmless, but a menu +# that hides it is confusing. +catalog_installed() { + local target="$1" + if [[ "$target" == flatpak:* ]]; then + flatpak info "${target#flatpak:}" >/dev/null 2>&1 + else + rpm -q "$target" >/dev/null 2>&1 + fi +} diff --git a/setup/packages/extras/communication b/setup/packages/extras/communication index bc67aa9..0c18a48 100644 --- a/setup/packages/extras/communication +++ b/setup/packages/extras/communication @@ -1,8 +1,9 @@ -# Chat and messaging. Every one of these is Flathub only -- none is packaged +# Chat, mail and calls. Every one of these is Flathub only -- none is packaged # for Fedora, and each publishes its own flatpak. # # Thunderbird is deliberately absent: it is already in flatpak-packages as # org.mozilla.thunderbird_esr, which every machine gets. -flatpak:com.discordapp.Discord -flatpak:com.slack.Slack -flatpak:org.signal.Signal +flatpak:com.discordapp.Discord | Discord +flatpak:com.slack.Slack | Slack +flatpak:org.signal.Signal | Signal +flatpak:us.zoom.Zoom | Zoom diff --git a/setup/packages/extras/creative b/setup/packages/extras/creative index 6f13b49..596cfc1 100644 --- a/setup/packages/extras/creative +++ b/setup/packages/extras/creative @@ -1,7 +1,33 @@ -# Images, video and audio. All from dnf or RPM Fusion. -gimp +# Images, video and audio. +# +# Flatpaks rather than the dnf builds these used to name. That was not a +# preference: OBS's plugins are published as Flathub extensions and attach only +# to the flatpak, so the dnf build cannot have them at all. The rest follow so +# one machine does not end up with GIMP from dnf and its neighbour from Flathub. +flatpak:org.gimp.GIMP | GIMP +flatpak:org.kde.kdenlive | Kdenlive +flatpak:fr.handbrake.ghb | HandBrake +flatpak:com.orama_interactive.Pixelorama | Pixelorama +flatpak:org.gnome.gitlab.YaLTeR.VideoTrimmer | Video Trimmer +flatpak:org.gnome.gThumb | gThumb -# Video editing, screen capture, and transcoding what comes out of them. -kdenlive -obs-studio -HandBrake +# OBS and the plugins that make it usable for capture on Wayland. Ticking this +# installs all of them: they are extensions of the OBS flatpak, useless on their +# own, and choosing them one at a time is a menu nobody wants to read. +flatpak:com.obsproject.Studio | OBS Studio + flatpak:com.obsproject.Studio.Plugin.AdvancedMasks + flatpak:com.obsproject.Studio.Plugin.BackgroundRemoval + flatpak:com.obsproject.Studio.Plugin.CompositeBlur + flatpak:com.obsproject.Studio.Plugin.DownstreamKeyer + flatpak:com.obsproject.Studio.Plugin.GStreamerVaapi + flatpak:com.obsproject.Studio.Plugin.Gstreamer + flatpak:com.obsproject.Studio.Plugin.InputOverlay + flatpak:com.obsproject.Studio.Plugin.MoveTransition + flatpak:com.obsproject.Studio.Plugin.OBSPWVideo + flatpak:com.obsproject.Studio.Plugin.OBSVkCapture + flatpak:com.obsproject.Studio.Plugin.PipeWireAudioCapture + flatpak:com.obsproject.Studio.Plugin.SceneSwitcher + flatpak:com.obsproject.Studio.Plugin.Shaderfilter + flatpak:com.obsproject.Studio.Plugin.SourceClone + flatpak:com.obsproject.Studio.Plugin.SourceRecord + flatpak:com.obsproject.Studio.Plugin.WaylandHotkeys diff --git a/setup/packages/extras/gaming b/setup/packages/extras/gaming index f97fbcd..8663a29 100644 --- a/setup/packages/extras/gaming +++ b/setup/packages/extras/gaming @@ -1,5 +1,5 @@ # Games and the things that run them. A work laptop declines this whole -# category; a desktop wants all of it. +# category; a desktop wants most of it. # # steam is in RPM Fusion nonfree, which install-packages enables before it # reads this file. @@ -10,5 +10,15 @@ lutris mangohud gamescope -# Proton and Wine build management for Steam. Flathub only -- there is no RPM. -flatpak:com.vysp3r.ProtonPlus +# Proton and Wine build management. Flathub only -- there is no RPM. +flatpak:com.vysp3r.ProtonPlus | ProtonPlus +flatpak:com.usebottles.bottles | Bottles + +flatpak:com.mojang.Minecraft | Minecraft +flatpak:com.moonlight_stream.Moonlight | Moonlight + +# Flathub still carries yuzu and it still installs, but it is marked +# end-of-life upstream and receives no further work. Kept because it is on the +# machine this catalog was seeded from; said out loud so choosing it is a +# decision rather than a surprise. +flatpak:org.yuzu_emu.yuzu | yuzu (unmaintained) diff --git a/setup/packages/extras/modeling b/setup/packages/extras/modeling new file mode 100644 index 0000000..3d8395f --- /dev/null +++ b/setup/packages/extras/modeling @@ -0,0 +1,5 @@ +# CAD, slicing and game engines -- the things that produce files rather than +# consume them. +flatpak:org.freecad.FreeCAD | FreeCAD +flatpak:com.bambulab.BambuStudio | Bambu Studio +flatpak:org.godotengine.Godot | Godot diff --git a/setup/packages/extras/productivity b/setup/packages/extras/productivity new file mode 100644 index 0000000..f371300 --- /dev/null +++ b/setup/packages/extras/productivity @@ -0,0 +1,9 @@ +# Documents, notes and media playback. +# +# LibreOffice is the flatpak rather than Fedora's split packages: sunhat +# installed writer, calc and impress separately and got three-quarters of a +# suite, because the one nobody listed was the one eventually needed. +flatpak:org.libreoffice.LibreOffice | LibreOffice +flatpak:md.obsidian.Obsidian | Obsidian +flatpak:com.spotify.Client | Spotify +flatpak:tv.plex.PlexDesktop | Plex diff --git a/setup/packages/extras/utilities b/setup/packages/extras/utilities new file mode 100644 index 0000000..fe32915 --- /dev/null +++ b/setup/packages/extras/utilities @@ -0,0 +1,11 @@ +# Small tools for managing the machine itself. +# +# Flatseal edits flatpak permissions, Gear Lever manages AppImages, and +# Extension Manager is for the GNOME session -- which Panama no longer installs, +# so it is here to be chosen rather than in any core list. +flatpak:com.github.tchx84.Flatseal | Flatseal +flatpak:it.mijorus.gearlever | Gear Lever +flatpak:com.mattjakeman.ExtensionManager | Extension Manager +flatpak:org.localsend.localsend_app | LocalSend +flatpak:org.torproject.torbrowser-launcher | Tor Browser +flatpak:org.getmonero.Monero | Monero diff --git a/setup/scripts/install-packages b/setup/scripts/install-packages index 6e65c38..5eae70b 100755 --- a/setup/scripts/install-packages +++ b/setup/scripts/install-packages @@ -25,6 +25,11 @@ packages_in() { # --- Defined Paths --- PANAMA_PATH="$HOME/.local/share/Panama" +# Reading the extras catalog, shared with `panama apps` so the two front doors +# cannot disagree about what a category contains. +# shellcheck source=../lib/extras-catalog +source "$PANAMA_PATH/setup/lib/extras-catalog" + echo -e "\n--- Installing Repositories ---" log "Installing RPM Fusion Free and Nonfree Repositories" sudo dnf install -y https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm > /dev/null @@ -197,6 +202,11 @@ fi # package and a `flatpak:` line is a Flathub ID, so one file per category holds # the whole answer rather than splitting each category across two. # +# Reading the file is setup/lib/extras-catalog's job, not this function's, because +# `panama apps` offers the same catalog from the other side. Two parsers would +# eventually disagree about what a category contains, and the one that disagreed +# quietly would be this one -- it runs unattended. +# # Neither install is fatal. A category is a set of applications somebody wanted, # not a dependency of the desktop, and losing the rest of the run because one of # them was renamed upstream would be the wrong trade. @@ -205,8 +215,8 @@ install_extra_category() { name="$(basename "$file")" local dnf_packages flatpak_ids - dnf_packages=$(packages_in "$file" | tr ' ' '\n' | grep -v '^flatpak:' | tr "\n" " ") - flatpak_ids=$(packages_in "$file" | tr ' ' '\n' | sed -n 's/^flatpak://p' | tr "\n" " ") + dnf_packages=$(catalog_all_targets "$file" | grep -v '^flatpak:' | tr "\n" " ") + flatpak_ids=$(catalog_all_targets "$file" | sed -n 's/^flatpak://p' | tr "\n" " ") if [[ -n "${dnf_packages// /}" ]]; then log "Installing $name: $dnf_packages" diff --git a/tests/setup/extras-contract b/tests/setup/extras-contract index 9edbb80..5715e90 100755 --- a/tests/setup/extras-contract +++ b/tests/setup/extras-contract @@ -24,6 +24,14 @@ repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" installer="$repo_dir/setup/scripts/install-packages" interview="$repo_dir/setup/scripts/interview" extras_dir="$repo_dir/setup/packages/extras" +catalog="$repo_dir/setup/lib/extras-catalog" + +# The same parser both front doors use. A contract that re-implemented the +# format would eventually be testing its own idea of it rather than the one that +# runs -- which is exactly how the package lists came to be annotated with +# comments that every contract stripped and dnf did not. +# shellcheck source=../../setup/lib/extras-catalog +source "$catalog" findings=() note() { findings+=("$1"); } @@ -39,7 +47,7 @@ categories=("$extras_dir"/*) for category in "${categories[@]}"; do name="$(basename "$category")" [[ -f "$category" ]] || { note "$name is not a file"; continue; } - entries="$(sed 's/#.*//' "$category" | tr -d ' \t' | grep -cv '^$')" + entries="$(catalog_entries "$category" | grep -c . || true)" (( entries > 0 )) || note "the $name category installs nothing, so choosing it does nothing" done @@ -88,6 +96,7 @@ LIST ( PATH="$stub_dir:$PATH" log() { :; } + source "$catalog" eval "$filter" eval "$loop" install_extra_category "$fixture" @@ -112,6 +121,7 @@ fi PATH="$stub_dir:$PATH" PANAMA_PATH="$repo_dir" log() { :; } + source "$catalog" eval "$filter" eval "$loop" EXTRAS_DIR="$extras_dir" @@ -121,34 +131,113 @@ fi ) [[ -s "$calls" ]] && note 'with no categories chosen the installer still installed something' +# ── 3b. Labels and bundles ─────────────────────────────────────────────────── +# +# Two pieces of syntax carry real weight, and both fail quietly when wrong: a +# label that leaked into an install command would be handed to dnf as a package +# name, and a bundle that did not resolve would install OBS without the plugins +# that are the reason to pick it. + +bundle_fixture="$work/bundle" +cat >"$bundle_fixture" <<'LIST' +# A named entry, a bundle, and a plain one. +flatpak:com.example.Named | A Friendly Name +flatpak:com.example.Host | Host + flatpak:com.example.Host.Plugin.One + flatpak:com.example.Host.Plugin.Two +plain-package +LIST + +# An indented line belongs to the entry above it and must never be offered on +# its own, or the menu lists plugins as though they were applications. +selectable="$(catalog_entries "$bundle_fixture" | wc -l)" +(( selectable == 3 )) \ + || note "a category with 3 entries and 2 attached lines offers $selectable choices, not 3" +catalog_entries "$bundle_fixture" | grep -q 'Plugin.One' \ + && note 'an indented line is offered as a selectable application' + +# The label is for the menu and must not survive into an install target. +catalog_all_targets "$bundle_fixture" | grep -q '|' \ + && note 'a label reaches the install targets, where it would be treated as a package name' +catalog_entries "$bundle_fixture" | grep -q "$(printf 'flatpak:com.example.Named\tA Friendly Name')" \ + || note 'an explicit label is not carried through to the menu' + +# Ticking a bundle installs the entry and everything attached to it. +host_targets="$(catalog_targets "$bundle_fixture" "flatpak:com.example.Host" | wc -l)" +(( host_targets == 3 )) \ + || note "selecting a bundle resolves to $host_targets targets, not the entry plus its 2 attached lines" +catalog_targets "$bundle_fixture" "flatpak:com.example.Host" | grep -q '^flatpak:com.example.Host$' \ + || note 'selecting a bundle does not install the entry itself' + +# A plain entry stays plain: it must not absorb whatever follows it. +plain_targets="$(catalog_targets "$bundle_fixture" "plain-package" | wc -l)" +(( plain_targets == 1 )) \ + || note "a plain entry resolves to $plain_targets targets rather than just itself" + +# `panama apps` maps a selection back to an entry by its menu label, because +# that is all gum returns. Two entries sharing a label would therefore install +# whichever came first, silently and with no way to pick the other. +for category in "${categories[@]}"; do + [[ -f "$category" ]] || continue + duplicate="$(catalog_entries "$category" | cut -f2 | sort | uniq -d)" + [[ -z "$duplicate" ]] \ + || note "$(basename "$category") has more than one entry labelled '$duplicate', which makes the menu ambiguous" +done + +# ── 3c. Both front doors, one catalog ──────────────────────────────────────── +# +# `panama apps` and the interview offer the same applications. They diverge the +# moment either grows its own parser, and the divergence would be invisible. + +panama="$repo_dir/bin/panama" +grep -q 'source "$PANAMA_DIR/setup/lib/extras-catalog"' "$panama" \ + || note 'panama apps does not read the shared catalog' +grep -q 'source "$PANAMA_PATH/setup/lib/extras-catalog"' "$installer" \ + || note 'install-packages does not read the shared catalog' +grep -qE '^\s*apps\)' "$panama" \ + || note 'panama does not dispatch an apps subcommand' + # ── 4. Every name resolves ─────────────────────────────────────────────────── # # Skipped rather than failed when the repositories cannot be reached, so this # contract stays runnable on a train. +# +# One bulk query per manager, not one per name. Asking Flathub about forty-five +# ids individually took minutes and got slower every time an application was +# added -- a check nobody will wait for is a check that gets commented out. -if timeout 60 dnf list --available --quiet bash >/dev/null 2>&1; then - for category in "${categories[@]}"; do - [[ -f "$category" ]] || continue - while read -r package; do - [[ -n "$package" ]] || continue - [[ "$package" == flatpak:* ]] && continue - timeout 90 dnf list --quiet "$package" >/dev/null 2>&1 \ - || note "$(basename "$category") names $package, which dnf cannot resolve" - done < <(sed 's/#.*//' "$category" | tr -d ' \t' | grep -v '^$') - done +dnf_wanted="$(for category in "${categories[@]}"; do + [[ -f "$category" ]] && catalog_all_targets "$category" | grep -v '^flatpak:' +done | sort -u)" + +flatpak_wanted="$(for category in "${categories[@]}"; do + [[ -f "$category" ]] && catalog_all_targets "$category" | sed -n 's/^flatpak://p' +done | sort -u)" + +if [[ -n "$dnf_wanted" ]] && timeout 60 dnf list --available --quiet bash >/dev/null 2>&1; then + # repoquery answers for every name at once and simply omits the ones it + # cannot resolve, so the difference is the finding. + resolved="$(timeout 180 dnf repoquery --qf '%{name}\n' $dnf_wanted 2>/dev/null | sort -u)" + while read -r package; do + [[ -n "$package" ]] || continue + grep -qx "$package" <<<"$resolved" \ + || note "$package is named by a category but dnf cannot resolve it" + done <<<"$dnf_wanted" else printf 'extras contract: dnf is unreachable, so package names were not resolved\n' >&2 fi -if timeout 60 flatpak remote-info flathub org.mozilla.firefox >/dev/null 2>&1; then - for category in "${categories[@]}"; do - [[ -f "$category" ]] || continue - while read -r id; do - [[ -n "$id" ]] || continue - timeout 90 flatpak remote-info flathub "$id" >/dev/null 2>&1 \ - || note "$(basename "$category") names $id, which is not on Flathub" - done < <(sed 's/#.*//' "$category" | tr -d ' \t' | sed -n 's/^flatpak://p') - done +# --all matters: without it, remote-ls hides end-of-life applications, which +# still resolve and still install. Leaving it off reported yuzu as missing from +# Flathub when it is merely unmaintained -- a false negative that would have +# quietly deleted a working entry. +if [[ -n "$flatpak_wanted" ]] && timeout 120 flatpak remote-ls flathub --columns=application --all >"$work/flathub" 2>/dev/null \ + && [[ -s "$work/flathub" ]]; then + while read -r id; do + [[ -n "$id" ]] || continue + grep -qx "$id" "$work/flathub" \ + || note "$id is named by a category but is not on Flathub" + done <<<"$flatpak_wanted" else printf 'extras contract: Flathub is unreachable, so flatpak IDs were not resolved\n' >&2 fi