1414 lines
54 KiB
Bash
Executable File
1414 lines
54 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
# --- 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
|
|
|
|
# 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)"
|
|
|
|
# Establishing the verified ChatGPT repository, shared with the migration that
|
|
# replaces the community build, so neither can install it a less careful way.
|
|
# shellcheck source=../lib/chatgpt-package
|
|
source "$PANAMA_PATH/setup/lib/chatgpt-package"
|
|
|
|
# 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 --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 "$directory/.$(basename -- "$destination").link.XXXXXX")" || return 1
|
|
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
|
|
stage="$(mktemp -d "$parent/.v${INSTALLER_PROVENANCE[NODE_VERSION]}.stage.XXXXXX")" \
|
|
|| return 1
|
|
chmod 0700 "$stage"
|
|
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 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
|
|
stage="$(mktemp -d "$parent/.${INSTALLER_PROVENANCE[BUN_VERSION]}.stage.XXXXXX")" \
|
|
|| return 1
|
|
chmod 0700 "$stage"
|
|
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
|
|
stage="$(mktemp -d "$parent/.${INSTALLER_PROVENANCE[CODEX_VERSION]}.stage.XXXXXX")" \
|
|
|| return 1
|
|
chmod 0700 "$stage"
|
|
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 status=0
|
|
work=""
|
|
trap '[[ -z "$work" ]] || rm -rf -- "$work"' 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
|
|
work="$(mktemp -d)" || return 1
|
|
chmod 0700 "$work"
|
|
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; this exception applies only to the verified private local file
|
|
# and does not change signature policy for any repository.
|
|
sudo dnf install -y --setopt=localpkg_gpgcheck=0 "$rpm_path" >/dev/null || status=$?
|
|
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 "$directory/.${filename}.part.XXXXXX")" || exit 1
|
|
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" destination="$2" fingerprint_name="$3" expected="$4"
|
|
_require_policy_value "$fingerprint_name" "$expected" || return 1
|
|
cp -- "$source_key" "$destination" || return 1
|
|
chmod 0600 "$destination"
|
|
key_fingerprint_matches "$destination" "${INSTALLER_PROVENANCE[$fingerprint_name]}"
|
|
}
|
|
|
|
_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
|
|
}
|
|
|
|
# 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 staged_key="$1" key_destination="$2" staged_repo="$3" repo_destination="$4"
|
|
local backup_dir key_backup repo_backup key_mode=0644 repo_mode=0644
|
|
local key_current repo_current
|
|
local key_existed=0 repo_existed=0 status=0 rollback_status=0
|
|
|
|
[[ "$key_destination" == /etc/* && "$repo_destination" == /etc/* ]] || 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 -- "$staged_key")"
|
|
key_backup="$backup_dir/prior-key"
|
|
repo_backup="$backup_dir/prior-repo"
|
|
if [[ -e "$key_current" ]]; then
|
|
[[ -f "$key_current" ]] || return 1
|
|
cp -- "$key_current" "$key_backup" || return 1
|
|
key_mode="$(stat -c %a "$key_current")" || return 1
|
|
key_existed=1
|
|
fi
|
|
if [[ -e "$repo_current" ]]; then
|
|
[[ -f "$repo_current" ]] || return 1
|
|
cp -- "$repo_current" "$repo_backup" || return 1
|
|
repo_mode="$(stat -c %a "$repo_current")" || return 1
|
|
repo_existed=1
|
|
fi
|
|
|
|
sudo install -m 0644 "$staged_key" "$key_destination" || status=$?
|
|
if (( status == 0 )); then
|
|
sudo install -m 0644 "$staged_repo" "$repo_destination" || status=$?
|
|
fi
|
|
(( status == 0 )) && return 0
|
|
|
|
_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=$?
|
|
(( rollback_status == 0 )) || log "Repository activation rollback did not complete"
|
|
return "$status"
|
|
}
|
|
|
|
_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
|
|
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"
|
|
[[ -f "$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"
|
|
[[ -f "$config" ]] || return 1
|
|
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
|
|
[[ -f "$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
|
|
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
|
|
|
|
work="$(mktemp -d)" || return 1
|
|
chmod 0700 "$work"
|
|
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" \
|
|
|| ! rpm_signature_matches "$free_rpm" \
|
|
"$PANAMA_PATH/setup/provenance/keys/rpmfusion-free.asc" \
|
|
"${INSTALLER_PROVENANCE[RPMFUSION_FREE_FINGERPRINT]}" \
|
|
|| ! _download_bounded "${INSTALLER_PROVENANCE[RPMFUSION_NONFREE_RELEASE_URL]}" \
|
|
"${INSTALLER_PROVENANCE[RPMFUSION_NONFREE_RELEASE_MAX_BYTES]}" "$nonfree_rpm" \
|
|
|| ! rpm_signature_matches "$nonfree_rpm" \
|
|
"$PANAMA_PATH/setup/provenance/keys/rpmfusion-nonfree.asc" \
|
|
"${INSTALLER_PROVENANCE[RPMFUSION_NONFREE_FINGERPRINT]}"; then
|
|
rm -rf -- "$work"
|
|
return 1
|
|
fi
|
|
local status=0
|
|
sudo dnf install -y --setopt=localpkg_gpgcheck=1 "$free_rpm" "$nonfree_rpm" || status=$?
|
|
rm -rf -- "$work"
|
|
return "$status"
|
|
}
|
|
|
|
install_terra_repository() {
|
|
local work staged_key staged_repo status effective_status=0
|
|
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
|
|
if rpm -q terra-release >/dev/null 2>&1; then
|
|
log "terra-release is installed without one trusted enabled Terra repository"
|
|
return "$TERRA_TRUST_FAILURE_STATUS"
|
|
fi
|
|
work="$(mktemp -d)" || return 1
|
|
chmod 0700 "$work"
|
|
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; then
|
|
rm -rf -- "$work"
|
|
return 1
|
|
fi
|
|
sudo install -m 0644 "$staged_key" /etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama || {
|
|
rm -rf -- "$work"
|
|
return 1
|
|
}
|
|
printf '%s\n' \
|
|
'[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' > "$staged_repo"
|
|
chmod 0600 "$staged_repo"
|
|
status=0
|
|
sudo dnf install -y \
|
|
--repofrompath "terra,${INSTALLER_PROVENANCE[TERRA_BASEURL]}" \
|
|
--setopt=terra.pkg_gpgcheck=1 \
|
|
--setopt=terra.repo_gpgcheck=1 \
|
|
--setopt=terra.gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama \
|
|
terra-release || status=$?
|
|
if (( status == 0 )); then
|
|
sudo install -m 0644 "$staged_repo" /etc/yum.repos.d/terra.repo || status=$?
|
|
fi
|
|
if (( status == 0 )); then
|
|
effective_status=0
|
|
_terra_effective_status || effective_status=$?
|
|
(( effective_status == 0 )) || status="$TERRA_TRUST_FAILURE_STATUS"
|
|
fi
|
|
rm -rf -- "$work"
|
|
return "$status"
|
|
}
|
|
|
|
configure_hyprland_repository() {
|
|
local work staged_key staged_repo status
|
|
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
|
|
work="$(mktemp -d)" || return 1
|
|
chmod 0700 "$work"
|
|
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; then
|
|
rm -rf -- "$work"
|
|
return 1
|
|
fi
|
|
printf '%s\n' \
|
|
'[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' > "$staged_repo"
|
|
chmod 0600 "$staged_repo"
|
|
status=0
|
|
_publish_repository_pair \
|
|
"$staged_key" /etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland \
|
|
"$staged_repo" /etc/yum.repos.d/panama-hyprland.repo || status=$?
|
|
rm -rf -- "$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
|
|
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
|
|
work="$(mktemp -d)" || return 1
|
|
chmod 0700 "$work"
|
|
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 ! key_fingerprint_matches "$key_file" "${INSTALLER_PROVENANCE[FLATHUB_FINGERPRINT]}"; then
|
|
rm -rf -- "$work"
|
|
return 1
|
|
fi
|
|
status=0
|
|
sudo flatpak remote-add --if-not-exists --gpg-import="$key_file" flathub "$url" \
|
|
|| status=$?
|
|
if (( status == 0 )); then
|
|
_flathub_remote_status || status=$?
|
|
fi
|
|
rm -rf -- "$work"
|
|
return "$status"
|
|
}
|
|
|
|
_install_claude_code() {
|
|
local work staged_key staged_repo status
|
|
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
|
|
work="$(mktemp -d)" || return 1
|
|
chmod 0700 "$work"
|
|
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; then
|
|
rm -rf -- "$work"
|
|
return 1
|
|
fi
|
|
printf '%s\n' \
|
|
'[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' > "$staged_repo"
|
|
chmod 0600 "$staged_repo"
|
|
status=0
|
|
_publish_repository_pair \
|
|
"$staged_key" /etc/pki/rpm-gpg/RPM-GPG-KEY-claude-code-panama \
|
|
"$staged_repo" /etc/yum.repos.d/claude-code.repo || status=$?
|
|
if (( status == 0 )); then
|
|
sudo dnf install -y --repo=claude-code --repo=fedora --repo=updates \
|
|
--from-repo=claude-code claude-code || status=$?
|
|
fi
|
|
rm -rf -- "$work"
|
|
return "$status"
|
|
}
|
|
|
|
install_claude_code() {
|
|
_install_claude_code || _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 gpgcheck repo_gpgcheck gpgkey local_key
|
|
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"
|
|
if [[ ! -f "$repo_file" ]] \
|
|
|| ! baseurl="$(_ini_value "$repo_file" claude-desktop baseurl)" \
|
|
|| [[ "$baseurl" != "${INSTALLER_PROVENANCE[CLAUDE_DESKTOP_BASEURL]}" ]] \
|
|
|| ! 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:///* ]]; then
|
|
_claude_desktop_manual
|
|
return 0
|
|
fi
|
|
local_key="${gpgkey#file://}"
|
|
if [[ ! -f "$local_key" ]] \
|
|
|| ! key_fingerprint_matches "$PANAMA_PATH/setup/provenance/keys/claude-desktop.asc" \
|
|
"${INSTALLER_PROVENANCE[CLAUDE_DESKTOP_FINGERPRINT]}" \
|
|
|| ! key_fingerprint_matches "$local_key" \
|
|
"${INSTALLER_PROVENANCE[CLAUDE_DESKTOP_FINGERPRINT]}"; then
|
|
_claude_desktop_manual
|
|
return 0
|
|
fi
|
|
sudo dnf install -y claude-desktop-extra
|
|
}
|
|
|
|
# --- 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 --refresh > /dev/null
|
|
install_list core-packages "Core"
|
|
install_list server-packages "Server"
|
|
set +e
|
|
setup_node
|
|
install_pnpm
|
|
install_bun
|
|
install_claude_code
|
|
install_codex
|
|
set -e
|
|
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
|
|
soft "the RPM Fusion appstream metadata" sudo dnf install -y rpmfusion-\*-appstream-data
|
|
# Terra bootstraps itself: --repofrompath defines a throwaway repo just long
|
|
# enough to install terra-release, which then writes the real /etc/yum.repos.d
|
|
# entry. Doing that a second time is not harmless -- dnf5 refuses the whole
|
|
# transaction with 'Id is present more than once in the configuration', because
|
|
# the throwaway id collides with the one terra-release already installed.
|
|
#
|
|
# That is what killed a re-run on a machine Terra had already reached: this sits
|
|
# in the repository section, above everything, so `set -e` ended the stage
|
|
# before a single package was considered. An installer whose second run does
|
|
# less than its first is worse than one that never ran.
|
|
log "Installing Terra Repository"
|
|
install_terra_repository > /dev/null
|
|
|
|
echo -e "\n--- Installing relevant packages ---"
|
|
log "Updating all packages. This may take a while"
|
|
sudo dnf update -y --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_list desktop-packages "Desktop"
|
|
|
|
# --- 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
|
|
HYPR_PACKAGES=$(packages_in "$HYPR_FILE")
|
|
log "Installing Hyprland desktop packages"
|
|
echo -e "Includes the following packages:"
|
|
echo -e "$(<"$HYPR_FILE")"
|
|
sudo dnf install -y --setopt=install_weak_deps=False $HYPR_PACKAGES > /dev/null
|
|
log "Hyprland packages installed!"
|
|
else
|
|
log "Package list was not in specified path: $HYPR_FILE"
|
|
fi
|
|
|
|
# Said out loud, because the failure this guards against was silent. The stage
|
|
# used to die somewhere above this point and report one red line among twenty
|
|
# minutes of scrollback, and the machine looked installed until you tried to log
|
|
# into it.
|
|
if rpm -q hyprland >/dev/null 2>&1; then
|
|
log "Hyprland $(rpm -q --queryformat '%{VERSION}' hyprland) is installed."
|
|
else
|
|
log "Hyprland is NOT installed. Nothing below this point will give you a desktop."
|
|
exit 1
|
|
fi
|
|
|
|
# --- Codecs and multimedia ---------------------------------------------------
|
|
#
|
|
# Below the desktop and every one of them non-fatal, because none is a
|
|
# dependency of it and each can fail for reasons that have nothing to do with
|
|
# this repository -- a swap whose source package this spin never shipped, a
|
|
# group renamed upstream between Fedora releases.
|
|
#
|
|
# A trailing `&& sync` on the group update previously meant a failure was exempt
|
|
# from set -e as well (bash does not apply -e to the left of a && list), so it
|
|
# went unreported rather than being deliberately tolerated. It is deliberate now.
|
|
|
|
log "Updating core, multimedia, and sound-and-video groups"
|
|
soft "the multimedia group update" \
|
|
sudo dnf4 groupupdate -y 'core' 'multimedia' 'sound-and-video' \
|
|
--setop='install_weak_deps=False' \
|
|
--exclude='PackageKit-gstreamer-plugin' \
|
|
--allowerasing
|
|
sync
|
|
log "Swapping ffmpeg-free for ffmpeg"
|
|
soft "the ffmpeg swap" sudo dnf swap -y 'ffmpeg-free' 'ffmpeg' --allowerasing
|
|
log "Swapping mesa-va-drivers for mesa-va-drivers-freeworld"
|
|
soft "the mesa driver swap" sudo dnf swap -y mesa-va-drivers mesa-va-drivers-freeworld
|
|
log "Upgrading Multimedia group with optional packages"
|
|
soft "the optional Multimedia upgrade" sudo dnf4 group upgrade -y --with-optional Multimedia
|
|
log "Installing GStreamer plugins (bad, good, base)"
|
|
soft "the GStreamer plugins" \
|
|
sudo dnf install -y gstreamer1-plugins-{bad-\*,good-\*,base} \
|
|
--exclude=gstreamer1-plugins-bad-free-devel
|
|
|
|
# --- Install Development Packages needed for Neovim ---
|
|
DEV_FILE="$PANAMA_PATH/setup/packages/development-packages"
|
|
if [[ -f "$DEV_FILE" ]]; then
|
|
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 $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
|
|
install_claude_code
|
|
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
|
|
|
|
# ChatGPT Desktop: OpenAI ships an official Linux RPM now. Panama used to build
|
|
# a community wrapper from the macOS disk image -- it was `panama app
|
|
# chatgpt-desktop` -- because no packaged form existed; that build froze often
|
|
# and carried its own local rebuild daemon. The official package is strictly
|
|
# better: it comes from a repository, so it upgrades with every other package
|
|
# from then on.
|
|
#
|
|
# The repository and its signing key are established first, from the copy
|
|
# pinned in setup/keys/, so dnf verifies the metadata and the package before
|
|
# either reaches root. Upstream's own instructions do not allow that -- see
|
|
# setup/lib/chatgpt-package for why they are not followed here.
|
|
if rpm -q chatgpt >/dev/null 2>&1; then
|
|
log "ChatGPT Desktop already installed"
|
|
elif ! chatgpt_install_repository sudo; then
|
|
log "Could not establish the verified ChatGPT repository; skipping"
|
|
softly_failed+=("ChatGPT Desktop")
|
|
else
|
|
log "Installing ChatGPT Desktop..."
|
|
sudo dnf install -y chatgpt > /dev/null \
|
|
|| { log "ChatGPT Desktop install failed; skipping"; softly_failed+=("ChatGPT 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 $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
|