#!/usr/bin/env bash set -euo pipefail # Defer process-group interrupts until the active scoped transaction has run # its own rollback and cleanup traps, then leave this package stage unchanged. trap 'exit 130' INT trap 'exit 143' TERM # --- Helper functions --- log() { echo -e "\033[1;34m[INFO]\033[0m $*"; } exists() { command -v "$1" >/dev/null 2>&1; } # The package names in a list, without the comments that explain them. # # The lists are annotated -- which package exists for which settings page, why # an exception was made -- and those annotations are for whoever reads the file # next. dnf is not so forgiving: it does not ignore an argument it cannot # match, it reports "No match for argument: #" and exits 1, and with `set -e` # above that ends this stage on the first annotated list it reaches. # # It could not be seen from here. On a machine that already has everything, a # re-run matches every real name and fails only on the comments; and every # contract that reads these lists strips comments before comparing, so the # tests were reading a file this script was not. packages_in() { sed 's/#.*//' "$1" | tr "\n" " " } # Names a list asked for that still are not installed, so --skip-unavailable # above can never silently shrink a list: a skipped font is a warning somebody # reads, not an absence somebody debugs a month later. report_missing() { local file="$1" name missing=() for name in $(packages_in "$file"); do # Three ways a list entry can be satisfied: it is a package name # (rpm -q), a capability another package provides (--whatprovides, # e.g. wget -> wget2-wget), or a bare command name provided as a file # path (command -v, e.g. awk -> /usr/bin/awk from gawk, which # --whatprovides misses because the provide is the path, not the word). rpm -q --whatprovides "$name" >/dev/null 2>&1 && continue command -v "$name" >/dev/null 2>&1 && continue missing+=("$name") done (( ${#missing[@]} > 0 )) && log "WARNING: not available on this machine: ${missing[*]}" return 0 } # 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. # stdout only. Swallowing stderr here would hide the one line that says WHY a # step was stepped over -- and worse, every one of these runs under sudo, whose # password prompt is the thing you would be hiding on a machine that asks for # one. soft() { local what="$1"; shift "$@" >/dev/null || { 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 # extras catalog from a path that does not exist. PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}" # Kept as a named path so the hermetic contract can redirect reads after # sourcing this file. Normal installer execution always resets it to /etc. PANAMA_SYSTEM_ETC=/etc PANAMA_SYSTEM_FLATPAK_REPO=/var/lib/flatpak/repo PRIVILEGED_TMPDIR=/var/tmp BASE_REPO_ARGS=(--repo=fedora --repo=updates) RPMFUSION_REPO_ARGS=( "${BASE_REPO_ARGS[@]}" --repo=rpmfusion-free --repo=rpmfusion-free-updates --repo=rpmfusion-nonfree --repo=rpmfusion-nonfree-updates ) RPMFUSION_FROM_REPOS='fedora,updates,rpmfusion-free,rpmfusion-free-updates,rpmfusion-nonfree,rpmfusion-nonfree-updates' PACKAGE_REPO_ARGS=( "${BASE_REPO_ARGS[@]}" --from-repo=fedora,updates ) _root_snapshot_directory_is_safe() { local directory="$1" [[ "$(dirname -- "$directory")" == "$PRIVILEGED_TMPDIR" \ && "$(basename -- "$directory")" =~ ^panama-install\.[[:alnum:]]+$ ]] } _remove_root_snapshot() { local snapshot="$1" directory identity directory="$(dirname -- "$snapshot")" _root_snapshot_directory_is_safe "$directory" || return 1 sudo test -d "$directory" || return 1 sudo test ! -L "$directory" || return 1 identity="$(sudo stat -c '%u:%a' -- "$directory")" || return 1 [[ "$identity" == '0:700' || "$identity" == '0:711' ]] || return 1 if [[ "$identity" == '0:711' ]]; then sudo chmod 0700 "$directory" || return 1 identity="$(sudo stat -c '%u:%a' -- "$directory")" || return 1 [[ "$identity" == '0:700' ]] || return 1 fi if ! sudo rm -rf -- "$directory"; then printf 'Installer staging cleanup failed. Retained artifact: %s\n' \ "$directory" >&2 return 1 fi } _root_owned_regular_file_is_safe() { local file="$1" identity owner mode [[ -f "$file" && ! -L "$file" ]] || return 1 identity="$(stat -c '%u:%a' -- "$file")" || return 1 IFS=: read -r owner mode <<<"$identity" [[ "$owner" == 0 && "$mode" =~ ^[0-7]{3,4}$ ]] || return 1 (( (8#$mode & 0022) == 0 )) } _stable_file_digest() { local file="$1" output_name="$2" before after [[ -f "$file" && ! -L "$file" ]] || return 1 before="$(sha256sum -- "$file" | awk '{ print $1 }')" || return 1 after="$(sha256sum -- "$file" | awk '{ print $1 }')" || return 1 [[ "$before" =~ ^[0-9a-f]{64}$ && "$before" == "$after" ]] || return 1 printf -v "$output_name" '%s' "$before" } _make_private_directory() { local output_name="$1" template="$2" candidate printf -v "$output_name" '%s' '' candidate="$(mktemp -u -d "$template")" || return 1 printf -v "$output_name" '%s' "$candidate" if ! mkdir -m 700 -- "$candidate"; then printf -v "$output_name" '%s' '' return 1 fi } # Copy a digest-attested user file into a private root-owned directory, then # have the privileged adapter hash the exact immutable copy it will reopen. # The caller owns the random candidate before privileged creation starts. Its # already-armed transaction trap can therefore remove a directory even when a # signal interrupts mkdir, without a command-substitution handoff window. _stage_root_snapshot() { local source="$1" expected="$2" name="$3" output_name="$4" local directory="" snapshot actual identity printf -v "$output_name" '%s' '' [[ -f "$source" && ! -L "$source" && "$expected" =~ ^[0-9a-f]{64}$ \ && "$name" =~ ^[[:alnum:]_.-]+$ ]] || return 1 directory="$(mktemp -u "$PRIVILEGED_TMPDIR/panama-install.XXXXXX")" || return 1 _root_snapshot_directory_is_safe "$directory" || return 1 snapshot="$directory/$name" printf -v "$output_name" '%s' "$snapshot" if ! sudo mkdir -m 0700 -- "$directory" \ || ! sudo test -d "$directory" \ || ! sudo test ! -L "$directory" \ || ! identity="$(sudo stat -c '%u:%a' -- "$directory")" \ || [[ "$identity" != '0:700' ]] \ || ! sudo chmod 0700 "$directory" \ || ! sudo install -m 0444 "$source" "$snapshot" \ || ! actual="$(sudo sha256sum -- "$snapshot" | awk '{ print $1 }')" \ || [[ "$actual" != "$expected" ]]; then if _remove_root_snapshot "$snapshot" >/dev/null 2>&1; then printf -v "$output_name" '%s' '' fi return 1 fi } # Signed installer inputs are public, so their root-owned snapshot can be made # traversable just long enough for unprivileged GPG/rpmkeys to read it. The # file itself remains root-owned and read-only; after verification the random # directory is private again before any privileged consumer reopens it. _review_root_snapshots() { local snapshot status=0 hide_status=0 local -a snapshots=() while (( $# > 0 )) && [[ "$1" != -- ]]; do snapshots+=("$1") shift done (( ${#snapshots[@]} > 0 && $# > 1 )) || return 1 shift for snapshot in "${snapshots[@]}"; do _root_snapshot_directory_is_safe "$(dirname -- "$snapshot")" \ && sudo chmod 0711 "$(dirname -- "$snapshot")" || { status=1; break; } done if (( status == 0 )); then "$@" || status=$? fi for snapshot in "${snapshots[@]}"; do _root_snapshot_directory_is_safe "$(dirname -- "$snapshot")" || continue sudo chmod 0700 "$(dirname -- "$snapshot")" || hide_status=$? done (( hide_status == 0 )) || return 1 return "$status" } # Reviewed installer data and verification primitives. The config parser treats # every value as inert data and rejects unknown, duplicate, or missing fields. # shellcheck source=../lib/artifact-provenance source "$PANAMA_PATH/setup/lib/artifact-provenance" load_installer_provenance "$PANAMA_PATH/setup/provenance/installers.conf" # 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" # Which machine this is. A server takes the short path below: core tools, # node, the agents -- no third-party repos, no desktop, no flatpaks. # shellcheck source=../lib/machine-role source "$PANAMA_PATH/setup/lib/machine-role" ROLE="$(panama_role)" # One list, installed the way every list is installed: --skip-unavailable so a # single rotted name cannot cost the transaction, then report_missing so a # skipped name is a warning somebody reads. install_list() { local file="$PANAMA_PATH/setup/packages/$1" label="$2" packages if [[ -f "$file" ]]; then packages=$(packages_in "$file") log "Installing $label Packages" echo -e "Includes the following packages:" echo -e "$(<"$file")" sudo dnf install -y "${PACKAGE_REPO_ARGS[@]}" \ --skip-unavailable $packages > /dev/null report_missing "$file" log "$label packages installed!" else log "Package list was not in specified path: $file" fi } # --- Reviewed language runtimes and agent tools ------------------------------ _record_installer_failure() { local component="$1" log "$component install did not complete; continuing" softly_failed+=("$component") return 1 } _set_artifact_arch() { local machine_arch machine_arch="$(uname -m)" || return 1 case "$machine_arch" in x86_64) artifact_arch=X86_64 ;; aarch64) artifact_arch=AARCH64 ;; *) log "Unsupported architecture: $machine_arch"; return 1 ;; esac } _archive_path_is_safe() { local member="$1" [[ -n "$member" && "$member" != /* && "$member" != *'//'* && ! "$member" =~ (^|/)\.\.?(/|$) ]] } _archive_member_is_safe() { local member="$1" expected_top="$2" _archive_path_is_safe "$member" [[ "$member" == "$expected_top" || "$member" == "$expected_top/" \ || "$member" == "$expected_top/"* ]] } _bun_zip_entry_types_match() { local archive="$1" archive_top="$2" details entry_types details="$(unzip -Z -s "$archive")" || return 1 entry_types="$(awk -v directory="$archive_top/" -v binary="$archive_top/bun" ' $NF == directory || $NF == binary { print substr($1, 1, 1), $NF } ' <<<"$details")" || return 1 [[ "$entry_types" == "d $archive_top/"$'\n'"- $archive_top/bun" ]] } _tree_links_stay_inside() { local root="$1" link resolved scan_fd scan_pid scan_status=0 invalid=0 # Retain and wait for find's PID: a loop fed directly by process substitution # cannot otherwise distinguish an empty tree from a failed traversal. exec {scan_fd}< <(find "$root" -type l -print0) scan_pid=$! while IFS= read -r -d '' link <&"$scan_fd"; do resolved="$(realpath -m -- "$link")" || { invalid=1; continue; } [[ "$resolved" == "$root" || "$resolved" == "$root/"* ]] || invalid=1 done exec {scan_fd}<&- wait "$scan_pid" || scan_status=$? (( scan_status == 0 && invalid == 0 )) } _tree_hardlinks_stay_inside() { local root="$1" device inode link_count key scan_fd scan_pid scan_status=0 local invalid=0 local -A names_in_tree=() inode_links=() exec {scan_fd}< <(find "$root" -type f -printf '%D %i %n\n') scan_pid=$! while read -r device inode link_count <&"$scan_fd"; do key="$device:$inode" names_in_tree["$key"]=$(( ${names_in_tree[$key]:-0} + 1 )) inode_links["$key"]="$link_count" done exec {scan_fd}<&- wait "$scan_pid" || scan_status=$? (( scan_status == 0 )) || return 1 for key in "${!names_in_tree[@]}"; do [[ "${names_in_tree[$key]}" == "${inode_links[$key]}" ]] || invalid=1 done (( invalid == 0 )) } _atomic_symlink() ( local target="$1" destination="$2" directory temporary="" trap '[[ -z "$temporary" ]] || rm -f -- "$temporary"' EXIT trap 'exit 130' INT trap 'exit 143' TERM directory="$(dirname -- "$destination")" mkdir -p -- "$directory" || return 1 temporary="$(mktemp -u "$directory/.$(basename -- "$destination").link.XXXXXX")" \ || return 1 umask 077 if ! (set -o noclobber; : >"$temporary") 2>/dev/null; then temporary="" return 1 fi rm -f -- "$temporary" || return 1 ln -s -- "$target" "$temporary" || return 1 if ! mv -Tf -- "$temporary" "$destination"; then return 1 fi ) _activate_directory_no_replace() { local staged="$1" destination="$2" mv -Tn -- "$staged" "$destination" || return 1 [[ ! -e "$staged" && ! -L "$staged" && -d "$destination" && ! -L "$destination" ]] } _write_runtime_receipt() { local directory="$1" artifact_digest="$2" binary_digest="$3" local receipt="$directory/.panama-provenance" [[ -d "$directory" && ! -L "$directory" && ! -e "$receipt" && ! -L "$receipt" ]] \ || return 1 ( umask 077 && printf 'schema=1\nartifact_sha256=%s\nbinary_sha256=%s\n' \ "$artifact_digest" "$binary_digest" > "$receipt" ) } _runtime_receipt_matches() { local directory="$1" binary="$2" artifact_digest="$3" binary_digest="$4" local receipt="$directory/.panama-provenance" actual [[ -d "$directory" && ! -L "$directory" && -f "$receipt" && ! -L "$receipt" && -f "$binary" && ! -L "$binary" ]] || return 1 cmp -s "$receipt" <(printf 'schema=1\nartifact_sha256=%s\nbinary_sha256=%s\n' \ "$artifact_digest" "$binary_digest") || return 1 actual="$(sha256sum "$binary" | awk '{ print $1 }')" || return 1 [[ "$actual" == "$binary_digest" ]] } _load_nvm() { local nvm_script="$PANAMA_SYSTEM_ETC/profile.d/nvm.sh" [[ -s "$nvm_script" ]] || return 1 set +u # shellcheck source=/dev/null source "$nvm_script" set -u declare -F nvm >/dev/null } _install_node() ( local artifact_arch machine_arch archive_top parent target stage archive extract listing member local artifact_digest binary_digest staged_binary stage="" trap '[[ -z "$stage" ]] || rm -rf -- "$stage"' EXIT trap 'exit 130' INT trap 'exit 143' TERM _set_artifact_arch || return 1 _load_nvm || return 1 case "$artifact_arch" in X86_64) machine_arch=x64 ;; AARCH64) machine_arch=arm64 ;; esac archive_top="node-v${INSTALLER_PROVENANCE[NODE_VERSION]}-linux-$machine_arch" parent="${NVM_DIR:-$HOME/.nvm}/versions/node" target="$parent/v${INSTALLER_PROVENANCE[NODE_VERSION]}" artifact_digest="${INSTALLER_PROVENANCE[NODE_${artifact_arch}_SHA256]}" binary_digest="${INSTALLER_PROVENANCE[NODE_${artifact_arch}_BINARY_SHA256]}" if [[ -e "$target" || -L "$target" ]]; then _runtime_receipt_matches "$target" "$target/bin/node" \ "$artifact_digest" "$binary_digest" || return 1 [[ -x "$target/bin/node" && "$($target/bin/node --version 2>/dev/null)" == "v${INSTALLER_PROVENANCE[NODE_VERSION]}" ]] \ || return 1 nvm alias default "${INSTALLER_PROVENANCE[NODE_VERSION]}" >/dev/null 2>&1 || return 1 return 0 fi mkdir -p -- "$parent" || return 1 _make_private_directory stage \ "$parent/.v${INSTALLER_PROVENANCE[NODE_VERSION]}.stage.XXXXXX" || return 1 archive="$stage/artifact" extract="$stage/extract" mkdir -m 0700 "$extract" || { rm -rf -- "$stage"; return 1; } if ! download_sha256 "${INSTALLER_PROVENANCE[NODE_${artifact_arch}_URL]}" \ "${INSTALLER_PROVENANCE[NODE_${artifact_arch}_SHA256]}" \ "${INSTALLER_PROVENANCE[NODE_${artifact_arch}_MAX_BYTES]}" "$archive"; then rm -rf -- "$stage" return 1 fi listing="$(tar -tJf "$archive")" || { rm -rf -- "$stage"; return 1; } [[ -n "$listing" ]] || { rm -rf -- "$stage"; return 1; } while IFS= read -r member; do _archive_member_is_safe "$member" "$archive_top" \ || { rm -rf -- "$stage"; return 1; } done <<<"$listing" tar -xJf "$archive" --no-same-owner --no-same-permissions -C "$extract" \ || { rm -rf -- "$stage"; return 1; } _tree_links_stay_inside "$extract/$archive_top" \ || { rm -rf -- "$stage"; return 1; } _tree_hardlinks_stay_inside "$extract/$archive_top" \ || { rm -rf -- "$stage"; return 1; } staged_binary="$extract/$archive_top/bin/node" [[ "$(sha256sum "$staged_binary" | awk '{ print $1 }')" == "$binary_digest" ]] \ || { rm -rf -- "$stage"; return 1; } [[ -d "$extract/$archive_top" && ! -L "$extract/$archive_top" && -x "$staged_binary" && "$($staged_binary --version 2>/dev/null)" \ == "v${INSTALLER_PROVENANCE[NODE_VERSION]}" ]] \ || { rm -rf -- "$stage"; return 1; } _write_runtime_receipt "$extract/$archive_top" "$artifact_digest" "$binary_digest" \ || { rm -rf -- "$stage"; return 1; } _activate_directory_no_replace "$extract/$archive_top" "$target" \ || { rm -rf -- "$stage"; return 1; } _runtime_receipt_matches "$target" "$target/bin/node" \ "$artifact_digest" "$binary_digest" || { rm -rf -- "$stage"; return 1; } [[ -x "$target/bin/node" && "$($target/bin/node --version 2>/dev/null)" \ == "v${INSTALLER_PROVENANCE[NODE_VERSION]}" ]] \ || { rm -rf -- "$stage"; return 1; } _tree_links_stay_inside "$target" || { rm -rf -- "$stage"; return 1; } _tree_hardlinks_stay_inside "$target" || { rm -rf -- "$stage"; return 1; } rm -rf -- "$stage" stage="" nvm alias default "${INSTALLER_PROVENANCE[NODE_VERSION]}" >/dev/null 2>&1 ) install_node() { _install_node || _record_installer_failure Node } # Kept as the call-site name used by the desktop-first ordering contract. setup_node() { install_node } install_pnpm() { if require_reviewed_fedora_release \ && sudo dnf install -y --repo=fedora --repo=updates \ --from-repo=fedora,updates pnpm >/dev/null; then return 0 fi _record_installer_failure pnpm } _install_bun() ( local artifact_arch archive_top target bin_link parent stage archive listing local staged_binary version_dir artifact_digest binary_digest stage="" trap '[[ -z "$stage" ]] || rm -rf -- "$stage"' EXIT trap 'exit 130' INT trap 'exit 143' TERM _set_artifact_arch || return 1 case "$artifact_arch" in X86_64) archive_top=bun-linux-x64 ;; AARCH64) archive_top=bun-linux-aarch64 ;; esac version_dir="$HOME/.bun/versions/${INSTALLER_PROVENANCE[BUN_VERSION]}" target="$version_dir/bin/bun" bin_link="$HOME/.bun/bin/bun" artifact_digest="${INSTALLER_PROVENANCE[BUN_${artifact_arch}_SHA256]}" binary_digest="${INSTALLER_PROVENANCE[BUN_${artifact_arch}_BINARY_SHA256]}" if [[ -e "$version_dir" || -L "$version_dir" ]]; then _runtime_receipt_matches "$version_dir" "$target" \ "$artifact_digest" "$binary_digest" || return 1 [[ -x "$target" && "$($target --version 2>/dev/null)" == "${INSTALLER_PROVENANCE[BUN_VERSION]}" ]] \ || return 1 [[ -L "$bin_link" && "$(readlink -- "$bin_link")" == "$target" ]] \ || _atomic_symlink "$target" "$bin_link" return fi parent="$HOME/.bun/versions" mkdir -p -- "$parent" || return 1 _make_private_directory stage \ "$parent/.${INSTALLER_PROVENANCE[BUN_VERSION]}.stage.XXXXXX" || return 1 archive="$stage/artifact" if ! download_sha256 "${INSTALLER_PROVENANCE[BUN_${artifact_arch}_URL]}" \ "${INSTALLER_PROVENANCE[BUN_${artifact_arch}_SHA256]}" \ "${INSTALLER_PROVENANCE[BUN_${artifact_arch}_MAX_BYTES]}" "$archive"; then rm -rf -- "$stage" return 1 fi listing="$(unzip -Z1 "$archive")" || { rm -rf -- "$stage"; return 1; } [[ "$listing" == "$archive_top/"$'\n'"$archive_top/bun" ]] \ || { rm -rf -- "$stage"; return 1; } _bun_zip_entry_types_match "$archive" "$archive_top" \ || { rm -rf -- "$stage"; return 1; } while IFS= read -r member; do _archive_member_is_safe "$member" "$archive_top" \ || { rm -rf -- "$stage"; return 1; } done <<<"$listing" mkdir -m 0700 "$stage/extract" "$stage/version" "$stage/version/bin" \ || { rm -rf -- "$stage"; return 1; } unzip -q "$archive" -d "$stage/extract" || { rm -rf -- "$stage"; return 1; } staged_binary="$stage/extract/$archive_top/bun" [[ -f "$staged_binary" && ! -L "$staged_binary" && -x "$staged_binary" && "$(sha256sum "$staged_binary" | awk '{ print $1 }')" == "$binary_digest" && "$($staged_binary --version 2>/dev/null)" == "${INSTALLER_PROVENANCE[BUN_VERSION]}" ]] \ || { rm -rf -- "$stage"; return 1; } mv -- "$staged_binary" "$stage/version/bin/bun" \ || { rm -rf -- "$stage"; return 1; } _write_runtime_receipt "$stage/version" "$artifact_digest" "$binary_digest" \ || { rm -rf -- "$stage"; return 1; } _activate_directory_no_replace "$stage/version" "$version_dir" \ || { rm -rf -- "$stage"; return 1; } _runtime_receipt_matches "$version_dir" "$target" \ "$artifact_digest" "$binary_digest" || { rm -rf -- "$stage"; return 1; } [[ -x "$target" && "$($target --version 2>/dev/null)" \ == "${INSTALLER_PROVENANCE[BUN_VERSION]}" ]] \ || { rm -rf -- "$stage"; return 1; } rm -rf -- "$stage" stage="" _atomic_symlink "$target" "$bin_link" ) install_bun() { _install_bun || _record_installer_failure Bun } _codex_version_matches() { local binary="$1" output version_pattern output="$($binary --version 2>/dev/null)" || return 1 version_pattern="${INSTALLER_PROVENANCE[CODEX_VERSION]//./\\.}" [[ "$output" =~ (^|[^0-9])${version_pattern}([^0-9]|$) ]] } _install_codex() ( local artifact_arch version_dir target bin_link parent expected_listing member local stage archive listing staged_binary artifact_digest binary_digest stage="" trap '[[ -z "$stage" ]] || rm -rf -- "$stage"' EXIT trap 'exit 130' INT trap 'exit 143' TERM _set_artifact_arch || return 1 version_dir="$HOME/.local/lib/panama/codex/${INSTALLER_PROVENANCE[CODEX_VERSION]}" target="$version_dir/codex" bin_link="$HOME/.local/bin/codex" artifact_digest="${INSTALLER_PROVENANCE[CODEX_${artifact_arch}_SHA256]}" binary_digest="${INSTALLER_PROVENANCE[CODEX_${artifact_arch}_BINARY_SHA256]}" if [[ -e "$version_dir" || -L "$version_dir" ]]; then _runtime_receipt_matches "$version_dir" "$target" \ "$artifact_digest" "$binary_digest" || return 1 [[ -x "$target" ]] || return 1 _codex_version_matches "$target" || return 1 [[ -L "$bin_link" && "$(readlink -- "$bin_link")" == "$target" ]] \ || _atomic_symlink "$target" "$bin_link" return fi parent="$HOME/.local/lib/panama/codex" mkdir -p -- "$parent" || return 1 _make_private_directory stage \ "$parent/.${INSTALLER_PROVENANCE[CODEX_VERSION]}.stage.XXXXXX" || return 1 archive="$stage/artifact" if ! download_sha256 "${INSTALLER_PROVENANCE[CODEX_${artifact_arch}_URL]}" \ "${INSTALLER_PROVENANCE[CODEX_${artifact_arch}_SHA256]}" \ "${INSTALLER_PROVENANCE[CODEX_${artifact_arch}_MAX_BYTES]}" "$archive"; then rm -rf -- "$stage" return 1 fi listing="$(tar -tzf "$archive")" || { rm -rf -- "$stage"; return 1; } expected_listing=$'bin/\nbin/codex\nbin/codex-code-mode-host\ncodex-package.json\ncodex-path/\ncodex-path/rg\ncodex-resources/\ncodex-resources/bwrap\ncodex-resources/zsh/\ncodex-resources/zsh/bin/\ncodex-resources/zsh/bin/zsh' [[ "$listing" == "$expected_listing" ]] || { rm -rf -- "$stage"; return 1; } while IFS= read -r member; do _archive_path_is_safe "$member" || { rm -rf -- "$stage"; return 1; } done <<<"$listing" mkdir -m 0700 "$stage/extract" "$stage/version" \ || { rm -rf -- "$stage"; return 1; } tar -xzf "$archive" --no-same-owner --no-same-permissions -C "$stage/extract" \ || { rm -rf -- "$stage"; return 1; } _tree_links_stay_inside "$stage/extract" || { rm -rf -- "$stage"; return 1; } _tree_hardlinks_stay_inside "$stage/extract" || { rm -rf -- "$stage"; return 1; } staged_binary="$stage/extract/bin/codex" [[ -f "$staged_binary" && ! -L "$staged_binary" && -x "$staged_binary" && "$(sha256sum "$staged_binary" | awk '{ print $1 }')" == "$binary_digest" ]] \ || { rm -rf -- "$stage"; return 1; } _codex_version_matches "$staged_binary" \ || { rm -rf -- "$stage"; return 1; } mv -- "$staged_binary" "$stage/version/codex" \ || { rm -rf -- "$stage"; return 1; } _write_runtime_receipt "$stage/version" "$artifact_digest" "$binary_digest" \ || { rm -rf -- "$stage"; return 1; } _activate_directory_no_replace "$stage/version" "$version_dir" \ || { rm -rf -- "$stage"; return 1; } _runtime_receipt_matches "$version_dir" "$target" \ "$artifact_digest" "$binary_digest" || { rm -rf -- "$stage"; return 1; } _codex_version_matches "$target" || { rm -rf -- "$stage"; return 1; } rm -rf -- "$stage" stage="" _atomic_symlink "$target" "$bin_link" ) install_codex() { _install_codex || _record_installer_failure Codex } _install_rustdesk() ( local artifact_arch installed_version="" work rpm_path root_rpm="" status=0 work="" cleanup_rustdesk() { [[ -z "$root_rpm" ]] || _remove_root_snapshot "$root_rpm" || true [[ -z "$work" ]] || rm -rf -- "$work" } trap cleanup_rustdesk EXIT trap 'exit 130' INT trap 'exit 143' TERM _set_artifact_arch || return 1 if [[ "$artifact_arch" == AARCH64 ]]; then log "RustDesk ${INSTALLER_PROVENANCE[RUSTDESK_VERSION]} has no reviewed aarch64 RPM" return 1 fi installed_version="$(rpm -q --queryformat '%{VERSION}' rustdesk 2>/dev/null)" || true if [[ "$installed_version" == "${INSTALLER_PROVENANCE[RUSTDESK_VERSION]}" ]]; then return 0 fi _make_private_directory work "${TMPDIR:-/tmp}/panama-rustdesk.XXXXXX" || return 1 rpm_path="$work/rustdesk.rpm" if ! download_sha256 "${INSTALLER_PROVENANCE[RUSTDESK_X86_64_URL]}" \ "${INSTALLER_PROVENANCE[RUSTDESK_X86_64_SHA256]}" \ "${INSTALLER_PROVENANCE[RUSTDESK_X86_64_MAX_BYTES]}" "$rpm_path"; then rm -rf -- "$work" return 1 fi # RustDesk 1.4.9's reviewed RPM is unsigned. Its exact SHA-256 is the trust # assertion. Root rechecks a private snapshot against that reviewed digest, # and DNF receives only the snapshot rather than reopening user-owned bytes. if ! _stage_root_snapshot "$rpm_path" \ "${INSTALLER_PROVENANCE[RUSTDESK_X86_64_SHA256]}" rustdesk.rpm root_rpm; then return 1 fi sudo dnf install -y --repo=fedora --repo=updates \ --setopt=localpkg_gpgcheck=0 "$root_rpm" >/dev/null || status=$? if _remove_root_snapshot "$root_rpm"; then root_rpm="" else status=1 fi rm -rf -- "$work" work="" return "$status" ) install_rustdesk() { _install_rustdesk || _record_installer_failure RustDesk } # --- What was stepped over --------------------------------------------------- # # Tolerating a failure is only better than aborting on it if somebody is told. # The whole point of surviving a soft failure is that the rest gets installed # anyway -- but a machine missing something should say so once, here, rather # than be discovered a week later. report_soft_failures() { if (( ${#softly_failed[@]} > 0 )); then log "Installed, but these were stepped over:" printf ' - %s\n' "${softly_failed[@]}" log "None of them stops the machine, but this run is not recorded as" log "complete, so the next 'panama update' tries them again." # A step that did not complete has not happened. Exiting non-zero is what # keeps ./install from stamping the packages hash over the gaps -- stamped, # they would never be retried (the hash-skip would say nothing changed). exit 1 fi } # --- Reviewed third-party repositories ------------------------------------- _require_policy_value() { local name="$1" expected="$2" [[ "${INSTALLER_PROVENANCE[$name]:-}" == "$expected" ]] || { log "Installer provenance for $name does not match Panama's reviewed policy" return 1 } } require_reviewed_fedora_release() { local current _require_policy_value FEDORA_RELEASE 44 || return 1 current="$(rpm -E %fedora)" || return 1 [[ "$current" == "${INSTALLER_PROVENANCE[FEDORA_RELEASE]}" ]] || { log "Fedora $current is not reviewed for third-party repositories; expected ${INSTALLER_PROVENANCE[FEDORA_RELEASE]}" return 1 } } # RPM repository bootstrap packages and Flatpak descriptors are authenticated # after download rather than by a SHA-256 pin. Keep their untrusted bytes in a # private file, enforce the reviewed size limit, and publish the file only after # curl has completed successfully. _download_bounded() { local url="$1" max_bytes="$2" destination="$3" directory filename directory="$(dirname -- "$destination")" filename="$(basename -- "$destination")" ( local part="" trap '[[ -z "$part" ]] || rm -f -- "$part"' EXIT trap 'exit 130' INT trap 'exit 143' TERM [[ "$max_bytes" =~ ^[1-9][0-9]*$ && -d "$directory" ]] || exit 1 umask 077 part="$(mktemp -u "$directory/.${filename}.part.XXXXXX")" || exit 1 if ! (set -o noclobber; : >"$part") 2>/dev/null; then part="" exit 1 fi curl --fail --location --connect-timeout 10 --max-time 600 \ --max-filesize "$max_bytes" --output "$part" "$url" || exit 1 [[ -f "$part" && "$(stat -c %s "$part")" -le "$max_bytes" ]] || exit 1 mv -f -- "$part" "$destination" ) } _stage_reviewed_key() { local source_key="$1" staged_key="$2" fingerprint_name="$3" expected="$4" local root_output_name="$5" digest_output_name="$6" local source_digest root_snapshot _require_policy_value "$fingerprint_name" "$expected" || return 1 cp -- "$source_key" "$staged_key" || return 1 chmod 0600 "$staged_key" || return 1 _stable_file_digest "$staged_key" source_digest || return 1 _stage_root_snapshot "$staged_key" "$source_digest" \ "$(basename -- "$staged_key")" "$root_output_name" || return 1 root_snapshot="${!root_output_name}" printf -v "$digest_output_name" '%s' "$source_digest" if ! _review_root_snapshots "$root_snapshot" -- key_fingerprint_matches \ "$root_snapshot" "${INSTALLER_PROVENANCE[$fingerprint_name]}"; then if _remove_root_snapshot "$root_snapshot" >/dev/null 2>&1; then printf -v "$root_output_name" '%s' '' fi return 1 fi } _stage_signed_rpm() { local package="$1" key="$2" fingerprint="$3" package_name="$4" local package_output_name="$5" key_output_name="$6" local package_digest key_digest staged_key root_package="" root_key="" staged_key="$(dirname -- "$package")/$(basename -- "$key")" cp -- "$key" "$staged_key" || return 1 chmod 0600 "$staged_key" || return 1 _stable_file_digest "$package" package_digest || return 1 _stable_file_digest "$staged_key" key_digest || return 1 _stage_root_snapshot "$package" "$package_digest" "$package_name" \ "$package_output_name" || return 1 root_package="${!package_output_name}" _stage_root_snapshot "$staged_key" "$key_digest" \ "$(basename -- "$staged_key")" "$key_output_name" || { if _remove_root_snapshot "$root_package" >/dev/null 2>&1; then printf -v "$package_output_name" '%s' '' fi return 1 } root_key="${!key_output_name}" if ! _review_root_snapshots "$root_package" "$root_key" -- \ rpm_signature_matches "$root_package" "$root_key" "$fingerprint"; then if _remove_root_snapshot "$root_package" >/dev/null 2>&1; then printf -v "$package_output_name" '%s' '' fi if _remove_root_snapshot "$root_key" >/dev/null 2>&1; then printf -v "$key_output_name" '%s' '' fi return 1 fi } # Use a fresh repository identity and command-line trust settings so an # ambient file or DNF override for the installed convenience repo cannot # redirect the package transaction. The key DNF consumes is the same reviewed # root snapshot verified immediately before this call. _install_bound_repo_packages() ( local repo_id="$1" baseurl_name="$2" expected_baseurl="$3" source_key="$4" local fingerprint_name="$5" expected_fingerprint="$6" repo_gpgcheck="$7" local work="" staged_key root_key="" key_digest status=0 local -a options=() shift 7 while (( $# > 0 )) && [[ "$1" != -- ]]; do options+=("$1") shift done (( $# > 1 )) || return 1 shift [[ "$repo_id" =~ ^panama-bound-[a-z0-9-]+$ \ && "$repo_gpgcheck" =~ ^[01]$ ]] || return 1 _require_policy_value "$baseurl_name" "$expected_baseurl" || return 1 trap '[[ -z "$root_key" ]] || _remove_root_snapshot "$root_key" || true; [[ -z "$work" ]] || rm -rf -- "$work"' EXIT trap 'exit 130' INT trap 'exit 143' TERM _make_private_directory work "${TMPDIR:-/tmp}/panama-repo-install.XXXXXX" \ || return 1 staged_key="$work/$repo_id.asc" _stage_reviewed_key "$source_key" "$staged_key" "$fingerprint_name" \ "$expected_fingerprint" root_key key_digest || return 1 sudo dnf install -y \ --repofrompath "$repo_id,${INSTALLER_PROVENANCE[$baseurl_name]}" \ --repo="$repo_id" --repo=fedora --repo=updates --from-repo="$repo_id" \ --setopt="$repo_id.gpgcheck=1" \ --setopt="$repo_id.repo_gpgcheck=$repo_gpgcheck" \ --setopt="$repo_id.gpgkey=file://$root_key" \ "${options[@]}" "$@" || status=$? if _remove_root_snapshot "$root_key"; then root_key="" else status=1 fi rm -rf -- "$work" work="" return "$status" ) # Repository text is authored by this script. Hash the same argument stream # independently of the user-owned pathname so a swap before root staging # cannot redefine the bytes that privileged publication expects. _write_private_text_digest() { local destination="$1" output_name="$2" digest shift 2 printf '%s\n' "$@" > "$destination" || return 1 digest="$(printf '%s\n' "$@" | sha256sum | awk '{ print $1 }')" || return 1 [[ "$digest" =~ ^[0-9a-f]{64}$ ]] || return 1 chmod 0600 "$destination" || return 1 printf -v "$output_name" '%s' "$digest" } _ini_value() { local file="$1" wanted_section="$2" wanted_key="$3" local -a values=() mapfile -t values < <(awk -v wanted_section="$wanted_section" -v wanted_key="$wanted_key" ' function trim(value) { sub(/^[[:space:]]+/, "", value) sub(/[[:space:]]+$/, "", value) return value } { sub(/\r$/, "") line = trim($0) if (line == "" || line ~ /^[#;]/) next if (line ~ /^\[[^]]+\]$/) { section = substr(line, 2, length(line) - 2) next } equals = index(line, "=") if (tolower(section) == tolower(wanted_section) && equals > 1) { key = trim(substr(line, 1, equals - 1)) if (tolower(key) == tolower(wanted_key)) print trim(substr(line, equals + 1)) } } ' "$file") (( ${#values[@]} > 0 )) || return 1 [[ ${#values[@]} -eq 1 && -n "${values[0]}" ]] || return 2 printf '%s\n' "${values[0]}" } _ini_section_count() { local file="$1" wanted_section="$2" awk -v wanted_section="$wanted_section" ' function trim(value) { sub(/^[[:space:]]+/, "", value) sub(/[[:space:]]+$/, "", value) return value } { sub(/\r$/, "") line = trim($0) if (line ~ /^\[[^]]+\]$/) { section = substr(line, 2, length(line) - 2) if (tolower(section) == tolower(wanted_section)) count++ } } END { print count + 0 } ' "$file" } _ini_key_occurrence_count() { local file="$1" wanted_section="$2" wanted_key="$3" awk -v wanted_section="$wanted_section" -v wanted_key="$wanted_key" ' function trim(value) { sub(/^[[:space:]]+/, "", value) sub(/[[:space:]]+$/, "", value) return value } { sub(/\r$/, "") line = trim($0) if (line == "" || line ~ /^[#;]/) next if (line ~ /^\[[^]]+\]$/) { section = substr(line, 2, length(line) - 2) next } if (tolower(section) != tolower(wanted_section)) next equals = index(line, "=") if (equals > 0) { key = trim(substr(line, 1, equals - 1)) } else { split(line, words, /[[:space:]]+/) key = words[1] } if (tolower(key) == tolower(wanted_key)) count++ } END { print count + 0 } ' "$file" } _restore_repository_file() { local existed="$1" backup="$2" mode="$3" destination="$4" if (( existed )); then sudo install -m "$mode" "$backup" "$destination" else sudo rm -f -- "$destination" fi } _root_backup_repository_file() { local source="$1" backup="$2" before after before="$(sudo sha256sum -- "$source" | awk '{ print $1 }')" || return 1 [[ "$before" =~ ^[0-9a-f]{64}$ ]] || return 1 sudo install -m 0600 "$source" "$backup" || return 1 after="$(sudo sha256sum -- "$backup" | awk '{ print $1 }')" || return 1 [[ "$before" == "$after" ]] } _finish_repository_transaction() { local original_status=$? rollback_status=0 cleanup_status=0 # Once rollback starts, a repeated Ctrl-C/TERM must not interrupt restoration # between the two files. Ignored dispositions are inherited by the restore # commands, then disappear with this transactional subshell. trap '' INT TERM if (( mutation_started && ! transaction_committed )); then _restore_repository_file "$repo_existed" "$repo_backup" "$repo_mode" \ "$repo_destination" || rollback_status=$? _restore_repository_file "$key_existed" "$key_backup" "$key_mode" \ "$key_destination" || rollback_status=$? fi if (( rollback_status != 0 )); then log "Repository rollback failed; recovery evidence retained at $(dirname -- "$root_key") and $(dirname -- "$root_repo")" original_status="$TERRA_TRUST_FAILURE_STATUS" else [[ -z "$root_key" ]] || _remove_root_snapshot "$root_key" || cleanup_status=$? [[ -z "$root_repo" ]] || _remove_root_snapshot "$root_repo" || cleanup_status=$? (( cleanup_status == 0 )) || original_status=1 fi trap - EXIT exit "$original_status" } # A key and its repository file form one trust root. If either activation # write fails after touching its target, restore both prior files or return both # targets to absence before reporting failure. _publish_repository_pair() ( local root_key="$1" key_destination="$2" staged_repo="$3" repo_destination="$4" local key_digest="$5" repo_digest="$6" local before_repo_hook="${7:-:}" after_repo_hook="${8:-:}" local root_repo="" backup_dir key_backup repo_backup actual_key_digest local key_mode=0644 repo_mode=0644 local key_current repo_current local key_existed=0 repo_existed=0 status=0 local mutation_started=0 transaction_committed=0 trap _finish_repository_transaction EXIT trap 'exit 130' INT trap 'exit 143' TERM [[ "$key_destination" == /etc/* && "$repo_destination" == /etc/* \ && "$key_digest" =~ ^[0-9a-f]{64}$ \ && "$repo_digest" =~ ^[0-9a-f]{64}$ ]] || return 1 _root_snapshot_directory_is_safe "$(dirname -- "$root_key")" || return 1 actual_key_digest="$(sudo sha256sum -- "$root_key" | awk '{ print $1 }')" \ || return 1 [[ "$actual_key_digest" == "$key_digest" ]] || return 1 _stage_root_snapshot "$staged_repo" "$repo_digest" \ "$(basename -- "$staged_repo")" root_repo || return 1 key_current="$PANAMA_SYSTEM_ETC${key_destination#/etc}" repo_current="$PANAMA_SYSTEM_ETC${repo_destination#/etc}" [[ ! -L "$key_current" && ! -L "$repo_current" ]] || return 1 backup_dir="$(dirname -- "$root_key")" key_backup="$backup_dir/prior-key" repo_backup="$backup_dir/prior-repo" if [[ -e "$key_current" ]]; then [[ -f "$key_current" ]] || return 1 key_mode="$(stat -c %a "$key_current")" || return 1 _root_backup_repository_file "$key_destination" "$key_backup" || return 1 key_existed=1 fi if [[ -e "$repo_current" ]]; then [[ -f "$repo_current" ]] || return 1 repo_mode="$(stat -c %a "$repo_current")" || return 1 _root_backup_repository_file "$repo_destination" "$repo_backup" || return 1 repo_existed=1 fi mutation_started=1 sudo install -m 0644 "$root_key" "$key_destination" || status=$? if (( status == 0 )); then "$before_repo_hook" || status=$? fi if (( status == 0 )); then sudo install -m 0644 "$root_repo" "$repo_destination" || status=$? fi if (( status == 0 )); then "$after_repo_hook" || status=$? fi (( status == 0 )) || return "$status" transaction_committed=1 ) _effective_terra_key() { awk -v reviewed_baseurl="${INSTALLER_PROVENANCE[TERRA_BASEURL]}" ' function reset_block() { delete values delete seen in_block = 0 id = "" terra_like = 0 } function finish_block( key) { if (!in_block || !terra_like) return if (seen["enabled"] != 1) { bad = 1 return } if (values["enabled"] != "1") return enabled_count++ if (id != "terra") bad = 1 for (key in required) { if (seen[key] != 1) bad = 1 } if (values["baseurl"] != reviewed_baseurl || values["metalink"] != "" \ || values["mirrorlist"] != "" || values["gpgcheck"] != "1" \ || values["pkg_gpgcheck"] != "1" || values["repo_gpgcheck"] != "1" \ || values["gpgkey"] != "file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama") bad = 1 trusted_key = values["gpgkey"] } BEGIN { split("enabled baseurl metalink mirrorlist gpgcheck pkg_gpgcheck repo_gpgcheck gpgkey", fields) for (field_index in fields) required[fields[field_index]] = 1 reset_block() } /^======== ".*" repository configuration: ========$/ { finish_block() reset_block() saw_nonempty = 1 header_count++ id = $0 sub(/^======== "/, "", id) sub(/" repository configuration: ========$/, "", id) if (id == "") bad = 1 terra_like = (tolower(id) ~ /^terra/) in_block = 1 next } { if ($0 == "") next saw_nonempty = 1 if (!in_block || $0 ~ /^========/) { bad = 1 next } separator = index($0, " = ") if (separator > 0) { key = substr($0, 1, separator - 1) value = substr($0, separator + 3) } else if ($0 ~ /^[[:alnum:]_.-]+$/) { key = $0 value = "" } else { bad = 1 next } if (key !~ /^[[:alnum:]_.-]+$/) { bad = 1 next } if (terra_like && key in required) { seen[key]++ values[key] = value } } END { finish_block() if (saw_nonempty && header_count == 0) bad = 1 if (bad || enabled_count > 1) exit 2 if (enabled_count == 0) exit 1 print trusted_key } ' } # Status 0 is one trusted effective Terra identity, 1 is no enabled Terra # identity, and 2 is an unsafe, duplicated, or unreadable effective state. _terra_effective_status() { local dump gpgkey parse_status=0 local_key repo_file dump="$(LC_ALL=C dnf --quiet --no-plugins --dump-repo-config='*')" || return 2 gpgkey="$(printf '%s\n' "$dump" | _effective_terra_key)" || parse_status=$? (( parse_status == 0 )) || return "$parse_status" [[ "$gpgkey" == 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama' ]] || return 2 local_key="$PANAMA_SYSTEM_ETC/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama" repo_file="$PANAMA_SYSTEM_ETC/yum.repos.d/terra.repo" _root_owned_regular_file_is_safe "$repo_file" || return 2 _root_owned_regular_file_is_safe "$local_key" || return 2 key_fingerprint_matches "$PANAMA_PATH/setup/provenance/keys/terra44.asc" \ "${INSTALLER_PROVENANCE[TERRA_FINGERPRINT]}" \ && key_fingerprint_matches "$local_key" \ "${INSTALLER_PROVENANCE[TERRA_FINGERPRINT]}" \ || return 2 } TERRA_TRUST_FAILURE_STATUS=78 preflight_terra_trust() { local status=0 _require_policy_value TERRA_BASEURL 'https://repos.fyralabs.com/terra44' \ || return "$TERRA_TRUST_FAILURE_STATUS" _require_policy_value TERRA_FINGERPRINT AE09157A4DE88B497EA1D5D300CDAB43DE226D6F \ || return "$TERRA_TRUST_FAILURE_STATUS" _terra_effective_status || status=$? if (( status == 0 || status == 1 )); then return 0 fi log "Effective Terra repository configuration is not trusted; refusing all package work" return "$TERRA_TRUST_FAILURE_STATUS" } # Status 0 is trusted, 1 is absent, and 2 is present but untrusted or malformed. _flathub_remote_status() { local config section_count url gpg_verify summary_verify disabled disabled_status local alternate_key_count config="$PANAMA_SYSTEM_FLATPAK_REPO/config" [[ -e "$config" || -L "$config" ]] || return 1 _root_owned_regular_file_is_safe "$config" || return 2 section_count="$(_ini_section_count "$config" 'remote "flathub"')" || return 2 (( section_count > 0 )) || return 1 (( section_count == 1 )) || return 2 url="$(_ini_value "$config" 'remote "flathub"' url)" || return 2 gpg_verify="$(_ini_value "$config" 'remote "flathub"' gpg-verify)" || return 2 summary_verify="$(_ini_value "$config" 'remote "flathub"' gpg-verify-summary)" || return 2 [[ "$url" == 'https://dl.flathub.org/repo/' ]] || return 2 case "${gpg_verify,,}" in true|yes|1) ;; *) return 2 ;; esac case "${summary_verify,,}" in true|yes|1) ;; *) return 2 ;; esac disabled_status=0 disabled="$(_ini_value "$config" 'remote "flathub"' xa.disable)" || disabled_status=$? if (( disabled_status == 0 )); then case "${disabled,,}" in true|yes|1) return 2 ;; esac elif (( disabled_status != 1 )); then return 2 fi alternate_key_count="$(_ini_key_occurrence_count "$config" 'remote "flathub"' gpgkeypath)" \ || return 2 # The reviewed default keyring is the only permitted trust source. Empty, # duplicate, malformed, and nonempty alternate paths all fail closed. (( alternate_key_count == 0 )) || return 2 _root_owned_regular_file_is_safe \ "$PANAMA_SYSTEM_FLATPAK_REPO/flathub.trustedkeys.gpg" || return 2 key_fingerprint_matches "$PANAMA_SYSTEM_FLATPAK_REPO/flathub.trustedkeys.gpg" \ "${INSTALLER_PROVENANCE[FLATHUB_FINGERPRINT]}" || return 2 } install_rpmfusion_repositories() ( local work="" free_rpm nonfree_rpm local root_free="" root_nonfree="" root_free_key="" root_nonfree_key="" status=0 cleanup_rpmfusion() { [[ -z "$root_free" ]] || _remove_root_snapshot "$root_free" || true [[ -z "$root_nonfree" ]] || _remove_root_snapshot "$root_nonfree" || true [[ -z "$root_free_key" ]] || _remove_root_snapshot "$root_free_key" || true [[ -z "$root_nonfree_key" ]] || _remove_root_snapshot "$root_nonfree_key" || true [[ -z "$work" ]] || rm -rf -- "$work" } trap cleanup_rpmfusion EXIT trap 'exit 130' INT trap 'exit 143' TERM require_reviewed_fedora_release || return 1 _require_policy_value RPMFUSION_FREE_RELEASE_URL \ 'https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-44.noarch.rpm' || return 1 _require_policy_value RPMFUSION_FREE_RELEASE_MAX_BYTES 4194304 || return 1 _require_policy_value RPMFUSION_NONFREE_RELEASE_URL \ 'https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-44.noarch.rpm' || return 1 _require_policy_value RPMFUSION_NONFREE_RELEASE_MAX_BYTES 4194304 || return 1 _require_policy_value RPMFUSION_FREE_FINGERPRINT E9A491A3DE247814E7E067EAE06F8ECDD651FF2E || return 1 _require_policy_value RPMFUSION_NONFREE_FINGERPRINT 79BDB88F9BBF73910FD4095B6A2AF96194843C65 || return 1 _make_private_directory work "${TMPDIR:-/tmp}/panama-rpmfusion.XXXXXX" || return 1 free_rpm="$work/rpmfusion-free-release.rpm" nonfree_rpm="$work/rpmfusion-nonfree-release.rpm" if ! _download_bounded "${INSTALLER_PROVENANCE[RPMFUSION_FREE_RELEASE_URL]}" \ "${INSTALLER_PROVENANCE[RPMFUSION_FREE_RELEASE_MAX_BYTES]}" "$free_rpm"; then rm -rf -- "$work" work="" return 1 fi _stage_signed_rpm "$free_rpm" \ "$PANAMA_PATH/setup/provenance/keys/rpmfusion-free.asc" \ "${INSTALLER_PROVENANCE[RPMFUSION_FREE_FINGERPRINT]}" \ rpmfusion-free-release.rpm root_free root_free_key || return 1 if ! _download_bounded "${INSTALLER_PROVENANCE[RPMFUSION_NONFREE_RELEASE_URL]}" \ "${INSTALLER_PROVENANCE[RPMFUSION_NONFREE_RELEASE_MAX_BYTES]}" "$nonfree_rpm"; then return 1 fi _stage_signed_rpm "$nonfree_rpm" \ "$PANAMA_PATH/setup/provenance/keys/rpmfusion-nonfree.asc" \ "${INSTALLER_PROVENANCE[RPMFUSION_NONFREE_FINGERPRINT]}" \ rpmfusion-nonfree-release.rpm root_nonfree root_nonfree_key || return 1 # DNF verifies local RPMs with the system RPM keyring. A fresh Fedora host # does not have the RPM Fusion keys until these release RPMs install, so # import the already fingerprint-verified root snapshots before asking DNF # to repeat the signature check. sudo rpm --import "$root_free_key" || status=$? if (( status == 0 )); then sudo rpm --import "$root_nonfree_key" || status=$? fi if (( status == 0 )); then sudo dnf install -y --repo=fedora --repo=updates --setopt=localpkg_gpgcheck=1 \ "$root_free" "$root_nonfree" || status=$? fi if _remove_root_snapshot "$root_free"; then root_free=""; else status=1; fi if _remove_root_snapshot "$root_nonfree"; then root_nonfree=""; else status=1; fi if _remove_root_snapshot "$root_free_key"; then root_free_key=""; else status=1; fi if _remove_root_snapshot "$root_nonfree_key"; then root_nonfree_key=""; else status=1; fi rm -rf -- "$work" work="" return "$status" ) _verify_reviewed_terra_repository() { local status=0 _terra_effective_status || status=$? (( status == 0 )) } install_terra_repository() ( local work="" root_key="" staged_key staged_repo key_digest repo_digest status effective_status=0 trap '[[ -z "$root_key" ]] || _remove_root_snapshot "$root_key" || true; [[ -z "$work" ]] || rm -rf -- "$work"' EXIT trap 'exit 130' INT trap 'exit 143' TERM require_reviewed_fedora_release || return 1 _require_policy_value TERRA_BASEURL 'https://repos.fyralabs.com/terra44' || return 1 _require_policy_value TERRA_FINGERPRINT AE09157A4DE88B497EA1D5D300CDAB43DE226D6F || return 1 _terra_effective_status || effective_status=$? if (( effective_status == 0 )); then log "Terra repository already configured and verified" return 0 elif (( effective_status != 1 )); then log "Effective Terra repository configuration is not trusted" return "$TERRA_TRUST_FAILURE_STATUS" fi _make_private_directory work "${TMPDIR:-/tmp}/panama-terra.XXXXXX" || return 1 staged_key="$work/terra44.asc" staged_repo="$work/terra.repo" if ! _stage_reviewed_key \ "$PANAMA_PATH/setup/provenance/keys/terra44.asc" "$staged_key" \ TERRA_FINGERPRINT AE09157A4DE88B497EA1D5D300CDAB43DE226D6F \ root_key key_digest; then rm -rf -- "$work" return 1 fi _write_private_text_digest "$staged_repo" repo_digest \ '[terra]' \ 'name=Panama reviewed Terra 44' \ "baseurl=${INSTALLER_PROVENANCE[TERRA_BASEURL]}" \ 'enabled=1' \ 'gpgcheck=1' \ 'repo_gpgcheck=1' \ 'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama' || return 1 status=0 _publish_repository_pair \ "$root_key" /etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama \ "$staged_repo" /etc/yum.repos.d/terra.repo \ "$key_digest" "$repo_digest" \ : _verify_reviewed_terra_repository || status=$? rm -rf -- "$work" work="" (( status == 0 )) || return "$TERRA_TRUST_FAILURE_STATUS" ) configure_hyprland_repository() ( local work="" root_key="" staged_key staged_repo key_digest repo_digest status trap '[[ -z "$root_key" ]] || _remove_root_snapshot "$root_key" || true; [[ -z "$work" ]] || rm -rf -- "$work"' EXIT trap 'exit 130' INT trap 'exit 143' TERM require_reviewed_fedora_release || return 1 _require_policy_value HYPRLAND_COPR_BASEURL \ 'https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/fedora-$releasever-$basearch/' \ || return 1 _make_private_directory work "${TMPDIR:-/tmp}/panama-hyprland.XXXXXX" || return 1 staged_key="$work/hyprland-copr.asc" staged_repo="$work/panama-hyprland.repo" if ! _stage_reviewed_key \ "$PANAMA_PATH/setup/provenance/keys/hyprland-copr.asc" "$staged_key" \ HYPRLAND_COPR_FINGERPRINT 97E23476C89635135407C7D5E9BA41342C4B2995 \ root_key key_digest; then rm -rf -- "$work" return 1 fi _write_private_text_digest "$staged_repo" repo_digest \ '[panama-hyprland]' \ 'name=Panama reviewed Hyprland COPR' \ "baseurl=${INSTALLER_PROVENANCE[HYPRLAND_COPR_BASEURL]}" \ 'enabled=1' \ 'gpgcheck=1' \ 'repo_gpgcheck=0' \ 'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland' || return 1 status=0 _publish_repository_pair \ "$root_key" /etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland \ "$staged_repo" /etc/yum.repos.d/panama-hyprland.repo \ "$key_digest" "$repo_digest" || status=$? rm -rf -- "$work" work="" return "$status" ) ensure_flathub_remote() ( local work descriptor encoded key_file url no_gpg_verify gpg_verify local alternate_key_count status remote_status no_gpg_status gpg_status local key_digest root_key="" work="" cleanup_flathub() { [[ -z "$root_key" ]] || _remove_root_snapshot "$root_key" || true [[ -z "$work" ]] || rm -rf -- "$work" } trap cleanup_flathub EXIT trap 'exit 130' INT trap 'exit 143' TERM require_reviewed_fedora_release || return 1 _require_policy_value FLATHUB_DESCRIPTOR_URL 'https://flathub.org/repo/flathub.flatpakrepo' || return 1 _require_policy_value FLATHUB_DESCRIPTOR_MAX_BYTES 1048576 || return 1 _require_policy_value FLATHUB_FINGERPRINT 6E5C05D979C76DAF93C081354184DD4D907A7CAE || return 1 remote_status=0 _flathub_remote_status || remote_status=$? if (( remote_status == 0 )); then return 0 elif (( remote_status != 1 )); then log "Existing Flathub remote does not match Panama's reviewed trust policy" return 1 fi _make_private_directory work "${TMPDIR:-/tmp}/panama-flathub.XXXXXX" || return 1 descriptor="$work/flathub.flatpakrepo" key_file="$work/flathub-key.asc" if ! _download_bounded "${INSTALLER_PROVENANCE[FLATHUB_DESCRIPTOR_URL]}" \ "${INSTALLER_PROVENANCE[FLATHUB_DESCRIPTOR_MAX_BYTES]}" "$descriptor" \ || ! url="$(_ini_value "$descriptor" 'Flatpak Repo' Url)" \ || [[ "$url" != 'https://dl.flathub.org/repo/' ]] \ || ! encoded="$(_ini_value "$descriptor" 'Flatpak Repo' GPGKey)" \ || ! printf '%s' "$encoded" | base64 --decode > "$key_file"; then rm -rf -- "$work" return 1 fi no_gpg_status=0 no_gpg_verify="$(_ini_value "$descriptor" 'Flatpak Repo' NoGPGVerify)" \ || no_gpg_status=$? if (( no_gpg_status == 0 )); then case "${no_gpg_verify,,}" in true|yes|1) rm -rf -- "$work"; return 1 ;; esac elif (( no_gpg_status != 1 )); then rm -rf -- "$work" return 1 fi gpg_status=0 gpg_verify="$(_ini_value "$descriptor" 'Flatpak Repo' GPGVerify)" || gpg_status=$? if (( gpg_status == 0 )); then case "${gpg_verify,,}" in false|no|0) rm -rf -- "$work"; return 1 ;; esac elif (( gpg_status != 1 )); then rm -rf -- "$work" return 1 fi alternate_key_count="$(_ini_key_occurrence_count "$descriptor" 'Flatpak Repo' GPGKeyPath)" \ || alternate_key_count=1 if (( alternate_key_count != 0 )); then rm -rf -- "$work" return 1 fi if ! _stable_file_digest "$key_file" key_digest \ || ! _stage_root_snapshot "$key_file" "$key_digest" flathub-key.asc root_key \ || ! _review_root_snapshots "$root_key" -- key_fingerprint_matches \ "$root_key" "${INSTALLER_PROVENANCE[FLATHUB_FINGERPRINT]}"; then rm -rf -- "$work" work="" return 1 fi status=0 sudo flatpak remote-add --if-not-exists --gpg-import="$root_key" flathub "$url" \ || status=$? if (( status == 0 )); then _flathub_remote_status || status=$? fi if _remove_root_snapshot "$root_key"; then root_key="" else status=1 fi rm -rf -- "$work" work="" return "$status" ) _install_claude_code() ( local work="" root_key="" staged_key staged_repo key_digest repo_digest status trap '[[ -z "$root_key" ]] || _remove_root_snapshot "$root_key" || true; [[ -z "$work" ]] || rm -rf -- "$work"' EXIT trap 'exit 130' INT trap 'exit 143' TERM if command -v claude >/dev/null 2>&1; then log "Claude Code already installed at \"$(command -v claude)\"" return 0 fi require_reviewed_fedora_release || return 1 _require_policy_value CLAUDE_CODE_BASEURL 'https://downloads.claude.ai/claude-code/rpm/stable' || return 1 _make_private_directory work "${TMPDIR:-/tmp}/panama-claude-code.XXXXXX" || return 1 staged_key="$work/claude-code.asc" staged_repo="$work/claude-code.repo" if ! _stage_reviewed_key \ "$PANAMA_PATH/setup/provenance/keys/claude-code.asc" "$staged_key" \ CLAUDE_CODE_FINGERPRINT 31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE \ root_key key_digest; then rm -rf -- "$work" return 1 fi _write_private_text_digest "$staged_repo" repo_digest \ '[claude-code]' \ 'name=Claude Code' \ "baseurl=${INSTALLER_PROVENANCE[CLAUDE_CODE_BASEURL]}" \ 'enabled=1' \ 'gpgcheck=1' \ 'repo_gpgcheck=1' \ 'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-claude-code-panama' || return 1 status=0 _publish_repository_pair \ "$root_key" /etc/pki/rpm-gpg/RPM-GPG-KEY-claude-code-panama \ "$staged_repo" /etc/yum.repos.d/claude-code.repo \ "$key_digest" "$repo_digest" || status=$? if (( status == 0 )); then _install_bound_repo_packages panama-bound-claude-code \ CLAUDE_CODE_BASEURL 'https://downloads.claude.ai/claude-code/rpm/stable' \ "$PANAMA_PATH/setup/provenance/keys/claude-code.asc" \ CLAUDE_CODE_FINGERPRINT 31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE \ 1 -- claude-code || status=$? fi rm -rf -- "$work" work="" return "$status" ) install_claude_code() { local status=0 _install_claude_code || status=$? (( status == 0 )) && return 0 (( status != TERRA_TRUST_FAILURE_STATUS )) \ || return "$TERRA_TRUST_FAILURE_STATUS" _record_installer_failure "Claude Code" } _claude_desktop_manual() { log "Claude Desktop is optional; configure its reviewed local-key repository manually to install it" } install_claude_desktop_if_trusted() ( local repo_file baseurl enabled gpgcheck repo_gpgcheck gpgkey local_key local work="" staged_key root_key="" key_digest status=0 cleanup_claude_desktop() { [[ -z "$root_key" ]] || _remove_root_snapshot "$root_key" || true [[ -z "$work" ]] || rm -rf -- "$work" } trap cleanup_claude_desktop EXIT trap 'exit 130' INT trap 'exit 143' TERM require_reviewed_fedora_release || return 1 _require_policy_value CLAUDE_DESKTOP_BASEURL \ 'https://patrickjaja.github.io/claude-desktop-extra/rpm/' || return 1 _require_policy_value CLAUDE_DESKTOP_FINGERPRINT 825A7D15D78BABE45646D5DF382409F597908867 || return 1 repo_file="$PANAMA_SYSTEM_ETC/yum.repos.d/claude-desktop.repo" local_key="$PANAMA_SYSTEM_ETC/pki/rpm-gpg/RPM-GPG-KEY-claude-desktop-panama" if ! _root_owned_regular_file_is_safe "$repo_file" \ || ! _root_owned_regular_file_is_safe "$local_key" \ || [[ "$(_ini_section_count "$repo_file" claude-desktop)" != 1 ]] \ || ! baseurl="$(_ini_value "$repo_file" claude-desktop baseurl)" \ || [[ "$baseurl" != "${INSTALLER_PROVENANCE[CLAUDE_DESKTOP_BASEURL]}" ]] \ || ! enabled="$(_ini_value "$repo_file" claude-desktop enabled)" \ || [[ "$enabled" != 1 ]] \ || ! gpgcheck="$(_ini_value "$repo_file" claude-desktop gpgcheck)" \ || [[ "$gpgcheck" != 1 ]] \ || ! repo_gpgcheck="$(_ini_value "$repo_file" claude-desktop repo_gpgcheck)" \ || [[ "$repo_gpgcheck" != 1 ]] \ || ! gpgkey="$(_ini_value "$repo_file" claude-desktop gpgkey)" \ || [[ "$gpgkey" != 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-claude-desktop-panama' ]] \ || [[ "$(_ini_key_occurrence_count "$repo_file" claude-desktop metalink)" != 0 ]] \ || [[ "$(_ini_key_occurrence_count "$repo_file" claude-desktop mirrorlist)" != 0 ]] \ || ! key_fingerprint_matches "$local_key" \ "${INSTALLER_PROVENANCE[CLAUDE_DESKTOP_FINGERPRINT]}"; then _claude_desktop_manual return 0 fi _make_private_directory work "${TMPDIR:-/tmp}/panama-claude-desktop.XXXXXX" \ || return 1 staged_key="$work/claude-desktop.asc" _stage_reviewed_key \ "$PANAMA_PATH/setup/provenance/keys/claude-desktop.asc" "$staged_key" \ CLAUDE_DESKTOP_FINGERPRINT 825A7D15D78BABE45646D5DF382409F597908867 \ root_key key_digest || return 1 sudo dnf install -y \ --repofrompath "panama-claude-desktop,${INSTALLER_PROVENANCE[CLAUDE_DESKTOP_BASEURL]}" \ --repo=panama-claude-desktop --repo=fedora --repo=updates \ --from-repo=panama-claude-desktop \ --setopt=panama-claude-desktop.gpgcheck=1 \ --setopt=panama-claude-desktop.repo_gpgcheck=1 \ --setopt="panama-claude-desktop.gpgkey=file://$root_key" \ claude-desktop-extra || status=$? if _remove_root_snapshot "$root_key"; then root_key="" else status=1 fi rm -rf -- "$work" work="" return "$status" ) # Agent tools are optional unless repository recovery itself becomes # indeterminate. Ordinary failures are recorded by each wrapper and allow the # next tool; status 78 stops before any later downloader or package solver. install_optional_agent_tools() { local claude_status=0 setup_node || true install_pnpm || true install_bun || true install_claude_code || claude_status=$? (( claude_status != TERRA_TRUST_FAILURE_STATUS )) \ || return "$TERRA_TRUST_FAILURE_STATUS" install_codex || true } # Install a mixed desktop list without letting a third-party repository shadow # an unrelated package. Terra and Cisco names are kept in publisher-specific # transactions; everything else can resolve only from Fedora and RPM Fusion. install_desktop_package_file() { local file="${1:-$PANAMA_PATH/setup/packages/desktop-packages}" package local openh264_repo_file="$PANAMA_SYSTEM_ETC/yum.repos.d/fedora-cisco-openh264.repo" local -a general_packages=() terra_packages=() openh264_packages=() local -a GENERAL_REPO_ARGS=("${RPMFUSION_REPO_ARGS[@]}") if [[ ! -f "$file" ]]; then log "Package list was not in specified path: $file" return 0 fi for package in $(packages_in "$file"); do case "$package" in mozilla-openh264) openh264_packages+=("$package") ;; cascadiamono-nerd-fonts|espanso-wayland|firamono-nerd-fonts|ghostty|\ jetbrainsmono-nerd-fonts|nautilus-open-any-terminal|victormono-nerd-fonts) terra_packages+=("$package") ;; *) general_packages+=("$package") ;; esac done log "Installing Desktop Packages" echo -e "Includes the following packages:" echo -e "$(<"$file")" if [[ -f "$openh264_repo_file" && ! -L "$openh264_repo_file" ]]; then GENERAL_REPO_ARGS+=(--repo=fedora-cisco-openh264) fi if (( ${#general_packages[@]} > 0 )); then sudo dnf install -y "${GENERAL_REPO_ARGS[@]}" \ --from-repo=fedora,updates --skip-unavailable \ "${general_packages[@]}" > /dev/null fi if (( ${#terra_packages[@]} > 0 )); then _install_bound_repo_packages panama-bound-terra \ TERRA_BASEURL 'https://repos.fyralabs.com/terra44' \ "$PANAMA_PATH/setup/provenance/keys/terra44.asc" \ TERRA_FINGERPRINT AE09157A4DE88B497EA1D5D300CDAB43DE226D6F \ 1 --skip-unavailable -- "${terra_packages[@]}" > /dev/null fi if (( ${#openh264_packages[@]} > 0 )); then if [[ -f "$openh264_repo_file" && ! -L "$openh264_repo_file" ]]; then sudo dnf install -y "${BASE_REPO_ARGS[@]}" \ --repo=fedora-cisco-openh264 \ --from-repo=fedora-cisco-openh264 --skip-unavailable \ "${openh264_packages[@]}" > /dev/null else log "WARNING: Fedora Cisco OpenH264 repository is unavailable; skipping ${openh264_packages[*]}" fi fi report_missing "$file" log "Desktop packages installed!" } # The Hyprland list intentionally mixes Fedora/RPM Fusion tools with the # reviewed COPR and Terra. Bind every known publisher-only package to its own # repository so neither third party can replace a base package with the same # name. install_hyprland_package_file() { local file="${1:-$PANAMA_PATH/setup/packages/hyprland-packages}" package local -a general_packages=() copr_packages=() terra_packages=() if [[ ! -f "$file" ]]; then log "Package list was not in specified path: $file" return 0 fi for package in $(packages_in "$file"); do case "$package" in gpu-screen-recorder|grimblast|hypridle|hyprland|hyprland-guiutils|\ hyprland-uwsm|hyprlock|hyprpaper|hyprpicker|hyprpolkitagent|\ hyprpwcenter|hyprshutdown|hyprsunset|hyprsysteminfo|quickshell|uwsm|\ xdg-desktop-portal-hyprland) copr_packages+=("$package") ;; helium-browser-bin|mpvpaper|satty|vicinae) terra_packages+=("$package") ;; *) general_packages+=("$package") ;; esac done log "Installing Hyprland desktop packages" echo -e "Includes the following packages:" echo -e "$(<"$file")" if (( ${#general_packages[@]} > 0 )); then sudo dnf install -y "${RPMFUSION_REPO_ARGS[@]}" \ --from-repo=fedora,updates \ --setopt=install_weak_deps=False "${general_packages[@]}" > /dev/null fi if (( ${#copr_packages[@]} > 0 )); then _install_bound_repo_packages panama-bound-hyprland \ HYPRLAND_COPR_BASEURL \ 'https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/fedora-$releasever-$basearch/' \ "$PANAMA_PATH/setup/provenance/keys/hyprland-copr.asc" \ HYPRLAND_COPR_FINGERPRINT 97E23476C89635135407C7D5E9BA41342C4B2995 \ 0 --setopt=install_weak_deps=False -- "${copr_packages[@]}" > /dev/null fi if (( ${#terra_packages[@]} > 0 )); then _install_bound_repo_packages panama-bound-terra \ TERRA_BASEURL 'https://repos.fyralabs.com/terra44' \ "$PANAMA_PATH/setup/provenance/keys/terra44.asc" \ TERRA_FINGERPRINT AE09157A4DE88B497EA1D5D300CDAB43DE226D6F \ 1 --setopt=install_weak_deps=False -- "${terra_packages[@]}" > /dev/null fi report_missing "$file" log "Hyprland packages installed!" } # --- The server path --------------------------------------------------------- # # Everything a server runs is above this line plus the lists it installs. No # RPM Fusion, no Terra, no COPR, no multimedia, no flatpaks: those exist for a # desktop, and every one of them is a network dependency and a failure mode a # headless machine has no reason to carry. if [[ "${1:-}" == --trust-preflight ]]; then if preflight_terra_trust; then exit 0 else exit $? fi fi # Repeat the enclosing installer's early preflight at the package boundary so # a repository change made after startup cannot reach this stage's first DNF. if ! preflight_terra_trust; then exit "$TERRA_TRUST_FAILURE_STATUS" fi if [[ "$ROLE" == server ]]; then echo -e "\n--- Installing packages (server) ---" log "Updating all packages. This may take a while" sudo dnf update -y "${BASE_REPO_ARGS[@]}" --refresh > /dev/null install_list core-packages "Core" install_list server-packages "Server" set +e agent_tools_status=0 install_optional_agent_tools || agent_tools_status=$? set -e (( agent_tools_status != TERRA_TRUST_FAILURE_STATUS )) \ || exit "$TERRA_TRUST_FAILURE_STATUS" report_soft_failures exit 0 fi echo -e "\n--- Installing Repositories ---" log "Installing RPM Fusion Free and Nonfree Repositories" install_rpmfusion_repositories > /dev/null log "Enabling Fedora Cisco OpenH264 Repository" # soft: this repo does not exist on every spin, and its absence must not cost # the desktop -- the ordering rule at soft()'s definition applies to the # repository extras just as much as to the codec swaps below. soft "enabling the openh264 repository" sudo dnf config-manager setopt fedora-cisco-openh264.enabled=1 log "Installing RPM Fusion AppStream Metadata" soft "the core group update" sudo dnf update @core -y "${RPMFUSION_REPO_ARGS[@]}" soft "the RPM Fusion appstream metadata" sudo dnf install -y \ "${RPMFUSION_REPO_ARGS[@]}" \ --from-repo=rpmfusion-free,rpmfusion-free-updates,rpmfusion-nonfree,rpmfusion-nonfree-updates \ rpmfusion-\*-appstream-data # Panama publishes Terra's reviewed key and repository file directly as one # transaction. The terra-release RPM only carries that repository file, while # adding separate RPM database state that cannot be rolled back with the pair. log "Installing Terra Repository" install_terra_repository > /dev/null PACKAGE_REPO_ARGS=( "${RPMFUSION_REPO_ARGS[@]}" --from-repo="$RPMFUSION_FROM_REPOS" ) echo -e "\n--- Installing relevant packages ---" log "Updating all packages. This may take a while" sudo dnf update -y "${RPMFUSION_REPO_ARGS[@]}" --refresh > /dev/null # --- Install the shared core, then the desktop-only lists --- # --skip-unavailable throughout (inside install_list): dnf5 refuses a whole # transaction over one missing name, so a single rotted entry used to cost # every package in a list -- and the desktop below never installed. The # skipped names are reported afterwards rather than silently dropped. install_list core-packages "Core" install_list initial-packages "Initial" install_desktop_package_file # --- Install the Hyprland desktop --- # # Directly after desktop-packages and deliberately before anything optional. # The reviewed local repository below supplies these packages. 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 "Configuring the reviewed Hyprland repository" configure_hyprland_repository > /dev/null install_hyprland_package_file "$HYPR_FILE" 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 "${RPMFUSION_REPO_ARGS[@]}" \ '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' \ "${RPMFUSION_REPO_ARGS[@]}" --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 \ "${RPMFUSION_REPO_ARGS[@]}" log "Upgrading Multimedia group with optional packages" soft "the optional Multimedia upgrade" sudo dnf4 group upgrade -y \ "${RPMFUSION_REPO_ARGS[@]}" --with-optional Multimedia log "Installing GStreamer plugins (bad, good, base)" soft "the GStreamer plugins" \ sudo dnf install -y "${RPMFUSION_REPO_ARGS[@]}" \ 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 DEV_PACKAGES=$(packages_in "$DEV_FILE") log "Installing Development Packages. Mostly for Neovim." echo -e "Includes the following packages:" echo -e "$(<"$DEV_FILE")" soft "the development packages" sudo dnf install -y \ "${PACKAGE_REPO_ARGS[@]}" $DEV_PACKAGES log "Development packages installed!" else log "Package list was not in specified path: $DEV_FILE" fi set +e setup_node install_pnpm install_bun agent_tools_status=0 install_claude_code agent_tools_status=$? if (( agent_tools_status == TERRA_TRUST_FAILURE_STATUS )); then set -e exit "$TERRA_TRUST_FAILURE_STATUS" fi install_codex set -e # Claude Desktop remains optional. Panama never downloads its community setup # script; only a repository an operator has already configured with the exact # reviewed local key is eligible for installation. if ! install_claude_desktop_if_trusted; then log "Claude Desktop install failed; skipping" softly_failed+=("Claude Desktop") fi # The RPM ships rustdesk.service already enabled, which is what provides # unattended access; Panama deliberately does not start it a second time. install_rustdesk || true # --- Install Flatpak Packages --- FLATPAK_FILE="$PANAMA_PATH/setup/packages/flatpak-packages" if [[ -f "$FLATPAK_FILE" ]]; then FLATPAK_PACKAGES=$(packages_in "$FLATPAK_FILE") log "Adding Flathub remote" if ensure_flathub_remote; then log "Installing Flatpak Packages" echo -e "Includes the following packages:" echo -e "$(<"$FLATPAK_FILE")" # 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 "Flathub trust verification failed; Flatpak packages were not installed" softly_failed+=("Flathub") fi else log "Package list was not in specified path: $FLATPAK_FILE" fi # --- Install the extras that were chosen ------------------------------------ # # Everything above is what every Panama machine gets. This is what one machine # asked for: the interview offers the categories in setup/packages/extras/ as a # checklist and records the chosen names, so a work laptop does not acquire # emulators and a desktop does not skip Steam. # # Absent means none. That is what makes this stage safe to re-run by hand while # repairing one piece of a machine -- and it means a category is installed only # by an explicit answer, never by a default that drifted. # # A category mixes both package managers, because the applications do: some are # in Fedora or RPM Fusion and some publish only a flatpak. A bare line is a dnf # 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. install_extra_category() { local file="$1" name name="$(basename "$file")" local dnf_packages flatpak_ids # sed rather than grep -v: most categories are flatpak-only, and grep exits 1 # when it selects nothing, which set -e above turns into a dead stage. dnf_packages=$(catalog_all_targets "$file" | sed '/^flatpak:/d' | 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" sudo dnf install -y "${PACKAGE_REPO_ARGS[@]}" $dnf_packages \ > /dev/null || { log "Some $name packages did not install"; softly_failed+=("$name packages"); } fi if [[ -n "${flatpak_ids// /}" ]]; then log "Installing $name flatpaks: $flatpak_ids" if ensure_flathub_remote; then sudo flatpak install -y flathub $flatpak_ids > /dev/null \ || { log "Some $name flatpaks did not install"; softly_failed+=("$name flatpaks"); } else log "Flathub trust verification failed; $name flatpaks were not installed" softly_failed+=("$name flatpaks") fi fi } EXTRAS_DIR="$PANAMA_PATH/setup/packages/extras" for extra in ${PANAMA_EXTRAS:-}; do if [[ -f "$EXTRAS_DIR/$extra" ]]; then install_extra_category "$EXTRAS_DIR/$extra" else log "No such extras category: $extra" fi done report_soft_failures