Debian-Hyprland/update-hyprland.sh
Don Williams d298480443 feat: Hyprland 0.51.x upgrade tooling, central tags, docs update
Introduce focused Hyprland stack upgrade tooling and improve install
ordering for 0.51.x, with centralized version management and detailed
documentation for upgrading from 0.49/0.50.x to 0.51.1.

New scripts and modules
- update-hyprland.sh: Manage the Hyprland stack with:
  - --install / --dry-run build modes
  - --only and --skip for selective components
  - --with-deps to (re)install build deps
  - --set {KEY=TAG} and --restore tag backup support
  - --fetch-latest to pull latest GitHub release tags
  - --via-helper to delegate summary-only dry-runs
- dry-run-build.sh: Compile-only helper with summary output
- install-scripts/wayland-protocols-src.sh: Build wayland-protocols from
  source (>= 1.45) to satisfy Hyprland 0.51.x requirements

Core features
- Centralized tag management via hypr-tags.env; tags exported to all
  modules. Environment overrides remain first priority.
- Automatic dependency ordering for Hyprland 0.51.x:
  wayland-protocols-src → hyprland-protocols → hyprutils → hyprlang →
  aquamarine → hyprland
- Optional auto-fetch of latest tags on install runs that include
  hyprland (can be disabled via --no-fetch)
- Selective updates for targeted components and skip lists
- Dry-run mode to validate builds without installing

Installer integration
- install.sh reads hypr-tags.env and optionally refreshes tags.
- Ensures wayland-protocols-src is built before Hyprland.
- Maintains robust sequencing for the Hyprland stack.

Docs
- Debian-Hyprland-Install-Upgrade.md and .es.md:
  - Add explicit section: Upgrade 0.49/0.50.x → 0.51.1
  - Recommend: `./update-hyprland.sh --install --only hyprland`
  - Provide optional `--with-deps` and `--dry-run` flows
  - Add quick link anchor under Upgrade Workflows
- Clarify that full install via install.sh is not required for this
  upgrade unless optional modules need refresh or recovering from a
  partial/failed setup

Usage highlights
- Pin and upgrade to 0.51.1:
  ./update-hyprland.sh --set HYPRLAND=v0.51.1
  ./update-hyprland.sh --install --only hyprland
- Optional:
  ./update-hyprland.sh --with-deps --install --only hyprland
  ./update-hyprland.sh --dry-run --only hyprland

Notes
- Target OS remains Debian Trixie/SID; run as sudo-capable user (not
  root); ensure deb-src entries are enabled.
2025-10-09 16:51:11 -04:00

521 lines
18 KiB
Bash
Executable File

#!/usr/bin/env bash
# update-hyprland.sh
# Manage and build just the Hyprland stack (Hyprland + companion apps/libs)
# - Maintains a central tag file (hypr-tags.env) with versions
# - Can fetch latest release tags from GitHub and update hypr-tags.env
# - Can restore tags from a backup
# - Can run a dry-run build (compile only) or install build of the stack
#
# Usage examples:
# chmod +x ./update-hyprland.sh
# ./update-hyprland.sh --dry-run # compile-only using current tags
# ./update-hyprland.sh --install # compile + install using current tags
# ./update-hyprland.sh --fetch-latest --dry-run # refresh tags to latest, then dry-run
# ./update-hyprland.sh --set HYPRLAND=v0.51.1 --dry-run # set one or more tags
# ./update-hyprland.sh --restore --dry-run # restore most recent backup of tags and dry-run
# ./update-hyprland.sh --only hyprland,hyprutils --dry-run
# ./update-hyprland.sh --skip aquamarine --install
# ./update-hyprland.sh --with-deps --dry-run
# ./update-hyprland.sh --fetch-latest --via-helper # use dry-run-build.sh for a summary-only run
#
# Notes:
# - Requires curl; for --fetch-latest, jq is recommended (installed by 00-dependencies.sh)
# - Works from repo root; do not cd into install-scripts/
set -euo pipefail
REPO_ROOT=$(pwd)
TAGS_FILE="$REPO_ROOT/hypr-tags.env"
LOG_DIR="$REPO_ROOT/Install-Logs"
mkdir -p "$LOG_DIR"
TS=$(date +%F-%H%M%S)
SUMMARY_LOG="$LOG_DIR/update-hypr-$TS.log"
# Default module order (core first, then Hyprland)
DEFAULT_MODULES=(
hyprutils
hyprlang
wayland-protocols-src
aquamarine
hyprgraphics
hyprwayland-scanner
hyprland-protocols
hyprland-qt-support
hyprland-qtutils
hyprland
)
WITH_DEPS=0
DO_INSTALL=0
DO_DRY_RUN=0
FETCH_LATEST=0
RESTORE=0
VIA_HELPER=0
NO_FETCH=0
ONLY_LIST=""
SKIP_LIST=""
SET_ARGS=()
usage() {
sed -n '2,120p' "$0" | sed -n '/^# /p' | sed 's/^# \{0,1\}//'
}
ensure_tags_file() {
if [[ ! -f "$TAGS_FILE" ]]; then
echo "[INFO] Creating default tags file: $TAGS_FILE" | tee -a "$SUMMARY_LOG"
cat >"$TAGS_FILE" <<'EOF'
HYPRLAND_TAG=v0.50.1
AQUAMARINE_TAG=v0.9.2
HYPRUTILS_TAG=v0.8.2
HYPRLANG_TAG=v0.6.4
HYPRGRAPHICS_TAG=v0.1.5
HYPRWAYLAND_SCANNER_TAG=v0.4.5
HYPRLAND_PROTOCOLS_TAG=v0.6.4
HYPRLAND_QT_SUPPORT_TAG=v0.1.0
HYPRLAND_QTUTILS_TAG=v0.1.4
EOF
fi
}
backup_tags() {
ensure_tags_file
cp "$TAGS_FILE" "$TAGS_FILE.bak-$TS"
echo "[INFO] Backed up $TAGS_FILE to $TAGS_FILE.bak-$TS" | tee -a "$SUMMARY_LOG"
}
restore_tags() {
latest_bak=$(ls -1t "$TAGS_FILE".bak-* 2>/dev/null | head -n1 || true)
if [[ -z "$latest_bak" ]]; then
echo "[ERROR] No backup tags file found." | tee -a "$SUMMARY_LOG"
exit 1
fi
cp "$latest_bak" "$TAGS_FILE"
echo "[INFO] Restored tags from $latest_bak" | tee -a "$SUMMARY_LOG"
}
set_tags_from_args() {
ensure_tags_file
backup_tags
# load existing into assoc map
declare -A map
while IFS='=' read -r k v; do
[[ -z "$k" || "$k" =~ ^# ]] && continue
map[$k]="$v"
done <"$TAGS_FILE"
for kv in "${SET_ARGS[@]}"; do
key="${kv%%=*}"
val="${kv#*=}"
case "$key" in
HYPRLAND | hyprland) key=HYPRLAND_TAG ;;
AQUAMARINE | aquamarine) key=AQUAMARINE_TAG ;;
HYPRUTILS | hyprutils) key=HYPRUTILS_TAG ;;
HYPRLANG | hyprlang) key=HYPRLANG_TAG ;;
HYPRGRAPHICS | hyprgraphics) key=HYPRGRAPHICS_TAG ;;
HYPRWAYLAND_SCANNER | hyprwayland-scanner | hyprwayland_scanner) key=HYPRWAYLAND_SCANNER_TAG ;;
HYPRLAND_PROTOCOLS | hyprland-protocols | hyprland_protocols) key=HYPRLAND_PROTOCOLS_TAG ;;
HYPRLAND_QT_SUPPORT | hyprland-qt-support | hyprland_qt_support) key=HYPRLAND_QT_SUPPORT_TAG ;;
HYPRLAND_QTUTILS | hyprland-qtutils | hyprland_qtutils) key=HYPRLAND_QTUTILS_TAG ;;
esac
map[$key]="$val"
done
{
for k in "${!map[@]}"; do
echo "$k=${map[$k]}"
done | sort
} >"$TAGS_FILE"
echo "[INFO] Updated $TAGS_FILE with provided tags" | tee -a "$SUMMARY_LOG"
}
# Fetch latest release tags from GitHub for the stack
fetch_latest_tags() {
ensure_tags_file
backup_tags
# Require curl; jq is preferred. Fallback to grep/sed if jq is missing.
if ! command -v curl >/dev/null 2>&1; then
echo "[ERROR] curl is required." | tee -a "$SUMMARY_LOG"
exit 1
fi
# Read existing to respect pinned values (only update keys set to 'auto' or 'latest')
declare -A existing
while IFS='=' read -r k v; do
[[ -z "$k" || "$k" =~ ^# ]] && continue
existing[$k]="$v"
done <"$TAGS_FILE"
declare -A repos=(
[HYPRLAND_TAG]="hyprwm/Hyprland"
[AQUAMARINE_TAG]="hyprwm/aquamarine"
[HYPRUTILS_TAG]="hyprwm/hyprutils"
[HYPRLANG_TAG]="hyprwm/hyprlang"
[HYPRGRAPHICS_TAG]="hyprwm/hyprgraphics"
[HYPRWAYLAND_SCANNER_TAG]="hyprwm/hyprwayland-scanner"
[HYPRLAND_PROTOCOLS_TAG]="hyprwm/hyprland-protocols"
[HYPRLAND_QT_SUPPORT_TAG]="hyprwm/hyprland-qt-support"
[HYPRLAND_QTUTILS_TAG]="hyprwm/hyprland-qtutils"
)
declare -A tags
for key in "${!repos[@]}"; do
repo="${repos[$key]}"
url="https://api.github.com/repos/$repo/releases/latest"
echo "[INFO] Fetching latest tag for $repo" | tee -a "$SUMMARY_LOG"
body=$(curl -fsSL "$url" || true)
if [[ -z "$body" ]]; then
echo "[WARN] Empty response for $repo; leaving $key unchanged" | tee -a "$SUMMARY_LOG"
continue
fi
if command -v jq >/dev/null 2>&1; then
tag=$(printf '%s' "$body" | jq -r '.tag_name // empty')
else
tag=$(printf '%s' "$body" | grep -m1 '"tag_name"' | sed -E 's/.*"tag_name"\s*:\s*"([^"]+)".*/\1/')
fi
if [[ -n "$tag" ]]; then
tags[$key]="$tag"
else
echo "[WARN] Could not parse tag for $repo; leaving $key unchanged" | tee -a "$SUMMARY_LOG"
fi
done
# Merge into existing file
declare -A map
while IFS='=' read -r k v; do
[[ -z "$k" || "$k" =~ ^# ]] && continue
map[$k]="$v"
done <"$TAGS_FILE"
for k in "${!tags[@]}"; do
# Only override if pinned value is 'auto' or 'latest'
if [[ "${existing[$k]:-}" =~ ^(auto|latest)$ ]] || [[ -z "${existing[$k]:-}" ]]; then
map[$k]="${tags[$k]}"
fi
done
{
for k in "${!map[@]}"; do
echo "$k=${map[$k]}"
done | sort
} >"$TAGS_FILE"
echo "[INFO] Refreshed tags written to $TAGS_FILE" | tee -a "$SUMMARY_LOG"
}
# Build runner using module scripts. Uses env vars from TAGS_FILE.
run_stack() {
# shellcheck disable=SC1090
source "$TAGS_FILE"
# Export tags so child scripts inherit them
export HYPRLAND_TAG AQUAMARINE_TAG HYPRUTILS_TAG HYPRLANG_TAG HYPRGRAPHICS_TAG HYPRWAYLAND_SCANNER_TAG HYPRLAND_PROTOCOLS_TAG HYPRLAND_QT_SUPPORT_TAG HYPRLAND_QTUTILS_TAG WAYLAND_PROTOCOLS_TAG
# Optionally install dependencies (not dry-run)
if [[ $WITH_DEPS -eq 1 ]]; then
echo "[INFO] Installing dependencies via 00-dependencies.sh" | tee -a "$SUMMARY_LOG"
if ! "$REPO_ROOT/install-scripts/00-dependencies.sh"; then
echo "[ERROR] Dependencies installation failed." | tee -a "$SUMMARY_LOG"
exit 1
fi
fi
# Build module list based on --only/--skip
local modules
if [[ -n "$ONLY_LIST" ]]; then
IFS=',' read -r -a modules <<<"$ONLY_LIST"
else
modules=("${DEFAULT_MODULES[@]}")
fi
if [[ -n "$SKIP_LIST" ]]; then
IFS=',' read -r -a _skips <<<"$SKIP_LIST"
local filtered=()
for m in "${modules[@]}"; do
local skip_it=0
for s in "${_skips[@]}"; do
[[ "$m" == "$s" ]] && {
skip_it=1
break
}
done
[[ $skip_it -eq 0 ]] && filtered+=("$m")
done
modules=("${filtered[@]}")
fi
# Ensure core prerequisites are installed before hyprland on install runs
# Order: wayland-protocols-src, hyprland-protocols, hyprutils, hyprlang, aquamarine, hyprland
if [[ $DO_INSTALL -eq 1 ]]; then
# Auto-fetch latest tags for Hyprland stack unless disabled
if [[ $NO_FETCH -eq 0 ]]; then
# Detect whether hyprland is part of the run
need_fetch=0
for m in "${modules[@]}"; do
[[ "$m" == "hyprland" ]] && need_fetch=1
done
if [[ $need_fetch -eq 1 ]]; then
echo "[INFO] Auto-fetching latest tags for Hyprland stack" | tee -a "$SUMMARY_LOG"
fetch_latest_tags
fi
fi
local has_hl=0 has_aqua=0 has_wp=0 has_utils=0 has_lang=0 has_hlprot=0
for m in "${modules[@]}"; do
[[ "$m" == "hyprland" ]] && has_hl=1
[[ "$m" == "aquamarine" ]] && has_aqua=1
[[ "$m" == "wayland-protocols-src" ]] && has_wp=1
[[ "$m" == "hyprland-protocols" ]] && has_hlprot=1
[[ "$m" == "hyprutils" ]] && has_utils=1
[[ "$m" == "hyprlang" ]] && has_lang=1
done
if [[ $has_hl -eq 1 ]]; then
# ensure each prerequisite is present
[[ $has_wp -eq 0 ]] && modules=("wayland-protocols-src" "${modules[@]}")
[[ $has_hlprot -eq 0 ]] && modules=("hyprland-protocols" "${modules[@]}")
[[ $has_utils -eq 0 ]] && modules=("hyprutils" "${modules[@]}")
[[ $has_lang -eq 0 ]] && modules=("hyprlang" "${modules[@]}")
[[ $has_aqua -eq 0 ]] && modules=("aquamarine" "${modules[@]}")
# Reorder to exact sequence before hyprland
# Remove existing occurrences and rebuild in correct order
local tmp=()
local inserted_wp=0 inserted_hlprot=0 inserted_utils=0 inserted_lang=0 inserted_aqua=0
for m in "${modules[@]}"; do
if [[ "$m" == "wayland-protocols-src" ]]; then
if [[ $inserted_wp -eq 0 ]]; then
tmp+=("wayland-protocols-src")
inserted_wp=1
fi
elif [[ "$m" == "hyprland-protocols" ]]; then
if [[ $inserted_hlprot -eq 0 ]]; then
# ensure wayland-protocols-src before hyprland-protocols
if [[ $inserted_wp -eq 0 ]]; then
tmp+=("wayland-protocols-src")
inserted_wp=1
fi
tmp+=("hyprland-protocols")
inserted_hlprot=1
fi
elif [[ "$m" == "hyprutils" ]]; then
if [[ $inserted_utils -eq 0 ]]; then
# ensure protocols before utils
if [[ $inserted_wp -eq 0 ]]; then
tmp+=("wayland-protocols-src")
inserted_wp=1
fi
if [[ $inserted_hlprot -eq 0 ]]; then
tmp+=("hyprland-protocols")
inserted_hlprot=1
fi
tmp+=("hyprutils")
inserted_utils=1
fi
elif [[ "$m" == "hyprlang" ]]; then
if [[ $inserted_lang -eq 0 ]]; then
# ensure utils before lang
if [[ $inserted_utils -eq 0 ]]; then
if [[ $inserted_wp -eq 0 ]]; then
tmp+=("wayland-protocols-src")
inserted_wp=1
fi
if [[ $inserted_hlprot -eq 0 ]]; then
tmp+=("hyprland-protocols")
inserted_hlprot=1
fi
tmp+=("hyprutils")
inserted_utils=1
fi
tmp+=("hyprlang")
inserted_lang=1
fi
elif [[ "$m" == "aquamarine" ]]; then
if [[ $inserted_aqua -eq 0 ]]; then
# ensure lang before aquamarine
if [[ $inserted_lang -eq 0 ]]; then
if [[ $inserted_utils -eq 0 ]]; then
if [[ $inserted_wp -eq 0 ]]; then
tmp+=("wayland-protocols-src")
inserted_wp=1
fi
if [[ $inserted_hlprot -eq 0 ]]; then
tmp+=("hyprland-protocols")
inserted_hlprot=1
fi
tmp+=("hyprutils")
inserted_utils=1
fi
tmp+=("hyprlang")
inserted_lang=1
fi
tmp+=("aquamarine")
inserted_aqua=1
fi
elif [[ "$m" == "hyprland" ]]; then
# ensure all prerequisites already present
if [[ $inserted_wp -eq 0 ]]; then
tmp+=("wayland-protocols-src")
inserted_wp=1
fi
if [[ $inserted_hlprot -eq 0 ]]; then
tmp+=("hyprland-protocols")
inserted_hlprot=1
fi
if [[ $inserted_utils -eq 0 ]]; then
tmp+=("hyprutils")
inserted_utils=1
fi
if [[ $inserted_lang -eq 0 ]]; then
tmp+=("hyprlang")
inserted_lang=1
fi
if [[ $inserted_aqua -eq 0 ]]; then
tmp+=("aquamarine")
inserted_aqua=1
fi
tmp+=("hyprland")
else
tmp+=("$m")
fi
done
modules=("${tmp[@]}")
fi
fi
declare -A results
for mod in "${modules[@]}"; do
local script="$REPO_ROOT/install-scripts/$mod.sh"
echo "\n=== $mod ===" | tee -a "$SUMMARY_LOG"
[[ -f "$script" ]] || {
echo "[WARN] Missing $script" | tee -a "$SUMMARY_LOG"
results[$mod]="MISSING"
continue
}
chmod +x "$script" || true
if [[ $DO_DRY_RUN -eq 1 ]]; then
if DRY_RUN=1 "$script"; then results[$mod]="PASS"; else results[$mod]="FAIL"; fi
else
if "$script"; then results[$mod]="INSTALLED"; else results[$mod]="FAIL"; fi
fi
done
{
echo "\nSummary:"
for mod in "${modules[@]}"; do
printf "%-24s %s\n" "$mod" "${results[$mod]:-SKIPPED}"
done
echo "\nLogs under: $LOG_DIR. This run: $SUMMARY_LOG"
} | tee -a "$SUMMARY_LOG"
# Non-zero on any FAILs
local failed=0
for mod in "${modules[@]}"; do
[[ "${results[$mod]:-}" == FAIL ]] && failed=1
done
return $failed
}
# Parse args
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
usage
exit 0
;;
--with-deps)
WITH_DEPS=1
shift
;;
--dry-run)
DO_DRY_RUN=1
shift
;;
--install)
DO_INSTALL=1
shift
;;
--fetch-latest)
FETCH_LATEST=1
shift
;;
--restore)
RESTORE=1
shift
;;
--via-helper)
VIA_HELPER=1
shift
;;
--no-fetch)
NO_FETCH=1
shift
;;
--only)
ONLY_LIST=${2:-}
shift 2
;;
--skip)
SKIP_LIST=${2:-}
shift 2
;;
--set)
shift
while [[ $# -gt 0 && "$1" != --* ]]; do
SET_ARGS+=("$1")
shift
done
;;
*)
echo "Unknown argument: $1"
exit 2
;;
esac
done
# Validate options
if [[ $DO_INSTALL -eq 1 && $DO_DRY_RUN -eq 1 ]]; then
echo "[ERROR] Use either --dry-run or --install, not both." | tee -a "$SUMMARY_LOG"
exit 2
fi
ensure_tags_file
# Apply tag operations
if [[ $RESTORE -eq 1 ]]; then
restore_tags
fi
if [[ ${#SET_ARGS[@]} -gt 0 ]]; then
set_tags_from_args
fi
if [[ $FETCH_LATEST -eq 1 ]]; then
fetch_latest_tags
fi
# Run the stack
if [[ $DO_DRY_RUN -eq 0 && $DO_INSTALL -eq 0 ]]; then
echo "[INFO] No build option specified. Defaulting to --dry-run." | tee -a "$SUMMARY_LOG"
DO_DRY_RUN=1
fi
# If using helper, delegate to dry-run-build.sh for summary-only output
if [[ $VIA_HELPER -eq 1 ]]; then
if [[ $DO_INSTALL -eq 1 ]]; then
echo "[ERROR] --via-helper cannot be combined with --install (helper is dry-run only)." | tee -a "$SUMMARY_LOG"
exit 2
fi
# shellcheck disable=SC1090
source "$TAGS_FILE"
export HYPRLAND_TAG AQUAMARINE_TAG HYPRUTILS_TAG HYPRLANG_TAG HYPRGRAPHICS_TAG HYPRWAYLAND_SCANNER_TAG HYPRLAND_PROTOCOLS_TAG HYPRLAND_QT_SUPPORT_TAG HYPRLAND_QTUTILS_TAG WAYLAND_PROTOCOLS_TAG
helper="$REPO_ROOT/dry-run-build.sh"
if [[ ! -x "$helper" ]]; then
echo "[ERROR] dry-run-build.sh not found or not executable at $helper" | tee -a "$SUMMARY_LOG"
exit 1
fi
args=()
[[ $WITH_DEPS -eq 1 ]] && args+=("--with-deps")
[[ -n "$ONLY_LIST" ]] && args+=("--only" "$ONLY_LIST")
[[ -n "$SKIP_LIST" ]] && args+=("--skip" "$SKIP_LIST")
echo "[INFO] Delegating to dry-run-build.sh ${args[*]}" | tee -a "$SUMMARY_LOG"
"$helper" "${args[@]}"
exit $?
fi
run_stack