Let applications be chosen a few at a time
The catalog held fourteen applications. This machine runs thirty-four flatpaks, so most of what is actually used had no way to be installed from here at all -- Zoom, Slack, Obsidian, Spotify, LibreOffice, OBS and its sixteen plugins. So the catalog is seeded from the machine, and `panama apps` opens it: pick a category, tick what you want, install just those. ./install still offers the same catalog as whole categories, because during a first install you want coarse and fast. Both read setup/lib/extras-catalog. Two parsers would eventually disagree about what a category contains, and the one that disagreed quietly would be the one that runs unattended. Two pieces of syntax earn their keep. A `| Name` suffix gives the menu something readable, since com.obsproject.Studio is not a name anybody wants to pick from a list. An indented line belongs to the entry above it, which is how OBS carries its plugins as one thing to tick rather than seventeen -- they are extensions of the flatpak, useless alone. That is also why creative moved from dnf to Flathub: the plugins attach only to the flatpak, so the dnf build cannot have them. The rest of the category followed rather than leave one machine with GIMP from dnf and its neighbour from Flathub. The contract now reads the catalog through the same parser instead of keeping a third idea of the format, and checks the two things this syntax can break silently: a label leaking into an install command, and a bundle that installs the application without its plugins. It caught a typo in the Pixelorama id on the first run. It also got slow enough to be worth fixing -- fifty-one names, each its own network call. One bulk query per manager took it from minutes to four seconds. That query needs `flatpak remote-ls --all`: without it, end-of-life applications are hidden and read as missing, which reported yuzu as gone from Flathub when it installs perfectly well. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
This commit is contained in:
+119
@@ -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" ;;
|
||||
*)
|
||||
|
||||
Executable
+107
@@ -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 `target<TAB>label`. 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
+105
-16
@@ -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
|
||||
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
|
||||
[[ "$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
|
||||
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
|
||||
# --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
|
||||
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
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user