cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix/website-download-wiring
goq / scripts / stage-bazzite-release.sh
stage-bazzite-release.sh 11.3K executable
1#!/usr/bin/env bash23set -euo pipefail4umask 07756usage() {7 cat <<'EOF'8Usage: scripts/stage-bazzite-release.sh \9 --release-id HEX \10 --host-binary PATH --host-sha256 HEX \11 --probe-binary PATH --probe-sha256 HEX1213Verify and atomically stage prebuilt x86_64 Linux Sigil binaries beneath:1415 ~/.local/libexec/sigil-spark/releases/<release-id>/1617The script changes the `current` symlink only after the installed `sigil`18executable, its byte-identical `sigil-host` compatibility copy, and19`sigil-probe` pass their hashes, dynamic-library check, and bounded startup20checks. It does not create an identity, install configuration, or start/enable21a service. It refuses to replace `current` when package-managed assets follow22that link; build the runtime package and use `payload/stage-this-release.sh`23for upgrades on a package-managed host.24EOF25}2627die() {28 printf 'stage release failed: %s\n' "$*" >&229 exit 130}3132release_id=""33host_binary=""34host_sha256=""35probe_binary=""36probe_sha256=""3738while [[ $# -gt 0 ]]; do39 case "$1" in40 --release-id)41 [[ $# -ge 2 ]] || die "--release-id requires a value"42 release_id="$2"43 shift 244 ;;45 --host-binary)46 [[ $# -ge 2 ]] || die "--host-binary requires a value"47 host_binary="$2"48 shift 249 ;;50 --host-sha256)51 [[ $# -ge 2 ]] || die "--host-sha256 requires a value"52 host_sha256="$2"53 shift 254 ;;55 --probe-binary)56 [[ $# -ge 2 ]] || die "--probe-binary requires a value"57 probe_binary="$2"58 shift 259 ;;60 --probe-sha256)61 [[ $# -ge 2 ]] || die "--probe-sha256 requires a value"62 probe_sha256="$2"63 shift 264 ;;65 --help|-h)66 usage67 exit 068 ;;69 *) die "unknown argument: $1" ;;70 esac71done7273[[ "$(uname -s)" == Linux ]] || die "this stager must run on Linux"74[[ "$(uname -m)" == x86_64 ]] || die "expected x86_64, got $(uname -m)"75[[ "$(id -u)" -ne 0 ]] || die "run as the dedicated gaming user, not root"76[[ "$release_id" =~ ^[0-9a-f]{7,64}$ ]] || die "release ID must be 7-64 lowercase hexadecimal characters"77[[ "$host_sha256" =~ ^[0-9a-f]{64}$ ]] || die "host SHA-256 must be 64 lowercase hexadecimal characters"78[[ "$probe_sha256" =~ ^[0-9a-f]{64}$ ]] || die "probe SHA-256 must be 64 lowercase hexadecimal characters"7980command -v cmp >/dev/null 2>&1 || die "cmp is required"81command -v sha256sum >/dev/null 2>&1 || die "sha256sum is required"82command -v ldd >/dev/null 2>&1 || die "ldd is required"83command -v timeout >/dev/null 2>&1 || die "timeout is required"8485validate_input() {86 local label="$1"87 local path="$2"88 local expected_sha256="$3"89 local actual_sha25690 local mode9192 [[ "$path" == /* ]] || die "$label path must be absolute"93 [[ ! -L "$path" ]] || die "$label must not be a symlink"94 [[ -f "$path" ]] || die "$label is not a regular file: $path"95 [[ "$(stat -Lc '%u' "$path")" -eq "$(id -u)" ]] || die "$label must be owned by the current user"96 mode="$(stat -Lc '%a' "$path")"97 (( (8#$mode & 8#022) == 0 )) || die "$label must not be group/world writable"9899 actual_sha256="$(sha256sum "$path" | awk '{print $1}')"100 [[ "$actual_sha256" == "$expected_sha256" ]] || die "$label SHA-256 mismatch"101}102103validate_input "host binary" "$host_binary" "$host_sha256"104validate_input "probe binary" "$probe_binary" "$probe_sha256"105106install_root="$HOME/.local/libexec/sigil-spark"107releases_root="$install_root/releases"108release_path="$releases_root/$release_id"109staging_path="$releases_root/.stage-$release_id-$$"110current_link="$install_root/current"111next_current="$install_root/.current-$release_id-$$"112previous_link="$install_root/previous"113next_previous="$install_root/.previous-$release_id-$$"114115# BEGIN release activation transaction helpers116activation_mutated=false117activation_committed=false118activation_reload_service=false119original_current_present=false120original_current_target=""121original_previous_present=false122original_previous_target=""123restore_current_temp="${current_link}.restore-$$"124restore_previous_temp="${previous_link}.restore-$$"125126snapshot_activation_links() {127 if [[ -L "$current_link" ]]; then128 original_current_present=true129 original_current_target="$(readlink "$current_link")"130 else131 original_current_present=false132 original_current_target=""133 fi134 if [[ -L "$previous_link" ]]; then135 original_previous_present=true136 original_previous_target="$(readlink "$previous_link")"137 else138 original_previous_present=false139 original_previous_target=""140 fi141}142143restore_activation_link() {144 local link="$1"145 local present="$2"146 local target="$3"147 local restore_temp="$4"148149 rm -f -- "$restore_temp"150 if [[ "$present" == true ]]; then151 ln -s "$target" "$restore_temp" || return 1152 mv -Tf -- "$restore_temp" "$link" || return 1153 elif [[ -L "$link" ]]; then154 rm -f -- "$link" || return 1155 elif [[ -e "$link" ]]; then156 return 1157 fi158}159160restore_activation_links() {161 if ! $activation_mutated || $activation_committed; then162 return 0163 fi164165 restore_activation_link \166 "$current_link" "$original_current_present" "$original_current_target" \167 "$restore_current_temp" || true168 restore_activation_link \169 "$previous_link" "$original_previous_present" "$original_previous_target" \170 "$restore_previous_temp" || true171 if $activation_reload_service; then172 systemctl --user daemon-reload >/dev/null 2>&1 || true173 fi174}175176activate_release_links() {177 activation_mutated=true178 if [[ -L "$next_previous" ]]; then179 mv -Tf -- "$next_previous" "$previous_link" || return 1180 fi181 mv -Tf -- "$next_current" "$current_link" || return 1182 if $activation_reload_service; then183 systemctl --user daemon-reload || return 1184 fi185 activation_committed=true186}187# END release activation transaction helpers188189# BEGIN package-managed activation guard190refuse_package_managed_activation() {191 local -a managed_links=(192 "$HOME/.config/pipewire/pipewire-pulse.conf.d/50-sigil-spark-audio.conf"193 "$HOME/.config/systemd/user/sigil-host.service"194 "$HOME/.local/bin/sigil-spark-host-rollback"195 "$HOME/.local/share/sigil-spark/package-assets/70-sigil-remote-input.rules"196 )197 local -a managed_targets=(198 "$install_root/current/assets/50-sigil-spark-audio.conf"199 "$install_root/current/assets/sigil-host.service"200 "$install_root/current/tools/rollback-bazzite-release.sh"201 "$install_root/current/assets/70-sigil-remote-input.rules"202 )203 local index204 local link_target205 local resolved_link206 local resolved_target207208 for index in "${!managed_links[@]}"; do209 if [[ -L "${managed_links[$index]}" ]]; then210 link_target="$(readlink "${managed_links[$index]}")"211 resolved_link="$(readlink -f "${managed_links[$index]}" 2>/dev/null || true)"212 resolved_target="$(readlink -f "${managed_targets[$index]}" 2>/dev/null || true)"213 if [[ "$link_target" == "${managed_targets[$index]}" \214 || ( -n "$resolved_link" && "$resolved_link" == "$resolved_target" ) ]]; then215 die "package-managed activation detected at ${managed_links[$index]}; build the runtime package and use payload/stage-this-release.sh"216 fi217 fi218 done219}220# END package-managed activation guard221222refuse_package_managed_activation223224for directory in "$HOME/.local" "$HOME/.local/libexec" "$install_root" "$releases_root"; do225 if [[ -e "$directory" ]]; then226 [[ -d "$directory" && ! -L "$directory" ]] || die "unsafe install directory: $directory"227 [[ "$(stat -Lc '%u' "$directory")" -eq "$(id -u)" ]] || die "install directory is not owned by current user: $directory"228 mode="$(stat -Lc '%a' "$directory")"229 (( (8#$mode & 8#022) == 0 )) || die "install directory is group/world writable: $directory"230 else231 install -d -m 0755 "$directory"232 fi233done234235cleanup() {236 local exit_status=$?237 trap - EXIT INT TERM HUP238 restore_activation_links239 if [[ -d "$staging_path" ]]; then240 rm -f -- "$staging_path/sigil" "$staging_path/sigil-host" "$staging_path/sigil-probe"241 rmdir -- "$staging_path" 2>/dev/null || true242 fi243 [[ ! -L "$next_current" ]] || rm -f -- "$next_current"244 [[ ! -L "$next_previous" ]] || rm -f -- "$next_previous"245 [[ ! -L "$restore_current_temp" ]] || rm -f -- "$restore_current_temp"246 [[ ! -L "$restore_previous_temp" ]] || rm -f -- "$restore_previous_temp"247 exit "$exit_status"248}249trap cleanup EXIT INT TERM HUP250251if [[ -e "$release_path" || -L "$release_path" ]]; then252 [[ -d "$release_path" && ! -L "$release_path" ]] || die "unsafe existing release path: $release_path"253 [[ "$(sha256sum "$release_path/sigil" | awk '{print $1}')" == "$host_sha256" ]] || die "existing release host hash differs"254 [[ "$(sha256sum "$release_path/sigil-host" | awk '{print $1}')" == "$host_sha256" ]] || die "existing release compatibility host hash differs"255 [[ "$(sha256sum "$release_path/sigil-probe" | awk '{print $1}')" == "$probe_sha256" ]] || die "existing release probe hash differs"256else257 install -d -m 0755 "$staging_path"258 install -m 0755 "$host_binary" "$staging_path/sigil"259 install -m 0755 "$host_binary" "$staging_path/sigil-host"260 install -m 0755 "$probe_binary" "$staging_path/sigil-probe"261 [[ "$(sha256sum "$staging_path/sigil" | awk '{print $1}')" == "$host_sha256" ]] || die "installed host hash differs"262 [[ "$(sha256sum "$staging_path/sigil-host" | awk '{print $1}')" == "$host_sha256" ]] || die "installed compatibility host hash differs"263 [[ "$(sha256sum "$staging_path/sigil-probe" | awk '{print $1}')" == "$probe_sha256" ]] || die "installed probe hash differs"264 mv -- "$staging_path" "$release_path"265fi266267cmp -s "$release_path/sigil" "$release_path/sigil-host" \268 || die "sigil-host compatibility executable differs from sigil"269ldd_output="$(ldd "$release_path/sigil")"270printf '%s\n' "$ldd_output"271grep -q 'not found' <<<"$ldd_output" && die "host binary has an unresolved shared library"272timeout --signal=TERM --kill-after=2s 5s "$release_path/sigil" --version273timeout --signal=TERM --kill-after=2s 5s "$release_path/sigil-probe" --help >/dev/null274275current_release_id=""276if [[ -e "$current_link" || -L "$current_link" ]]; then277 [[ -L "$current_link" ]] || die "current activation is not a symlink"278 current_path="$(readlink -f "$current_link")"279 [[ -n "$current_path" && -d "$current_path" ]] || die "current activation is dangling"280 # Fedora Atomic/Bazzite exposes /home through /var/home. Compare canonical281 # paths on both sides so the managed symlink does not appear to escape.282 [[ "$(dirname -- "$current_path")" == "$(readlink -f "$releases_root")" ]] \283 || die "current activation escapes the release root"284 current_release_id="$(basename -- "$current_path")"285 [[ "$current_release_id" =~ ^[0-9a-f]{7,64}$ ]] || die "current activation has an invalid release ID"286fi287288if [[ -e "$previous_link" || -L "$previous_link" ]]; then289 [[ -L "$previous_link" ]] || die "previous activation is not a symlink"290fi291snapshot_activation_links292293ln -s "releases/$release_id" "$next_current"294if [[ -n "$current_release_id" && "$current_release_id" != "$release_id" ]]; then295 ln -s "releases/$current_release_id" "$next_previous"296fi297activate_release_links298299printf 'release_id=%s\n' "$release_id"300printf 'host_sha256=%s\n' "$(sha256sum "$release_path/sigil" | awk '{print $1}')"301printf 'probe_sha256=%s\n' "$(sha256sum "$release_path/sigil-probe" | awk '{print $1}')"302printf 'current=%s\n' "$(readlink -f "$current_link")"303if [[ -L "$previous_link" ]]; then304 printf 'previous=%s\n' "$(readlink -f "$previous_link")"305else306 printf 'previous=none\n'307fi308printf 'release_activation=current-symlink-updated\n'309printf 'service_activation=not-attempted\n'