cat / goq
clone

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

1-n-sigls
goq / scripts / install-bazzite-package.sh
install-bazzite-package.sh 17.0K executable
1#!/usr/bin/env bash23set -euo pipefail4umask 07756usage() {7 cat <<'EOF'8Usage: install-bazzite-package.sh [--payload-dir PATH]910Verify and install one Sigil Bazzite host runtime package. The release is11fully staged and validated before the current symlink changes. New packages12contain the primary `sigil` executable and a byte-identical `sigil-host`13compatibility copy. Package-owned user assets follow current, so upgrades and14rollbacks keep binaries, service, and PipeWire configuration on the same15release.1617This command never creates or replaces host identity/configuration, restarts18PipeWire, starts/enables the host service, changes groups, or writes /etc.19EOF20}2122die() {23 printf 'package install failed: %s\n' "$*" >&224 exit 125}2627script_dir="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"28payload_dir="$script_dir"29readonly asset_target_contract="linux-glibc2.17-x86_64"3031while [[ $# -gt 0 ]]; do32 case "$1" in33 --payload-dir)34 [[ $# -ge 2 ]] || die "--payload-dir requires a path"35 payload_dir="$2"36 shift 237 ;;38 --help|-h)39 usage40 exit 041 ;;42 *) die "unknown argument: $1" ;;43 esac44done4546[[ "$(uname -s)" == Linux ]] || die "this package must be installed on Linux"47[[ "$(uname -m)" == x86_64 ]] || die "expected x86_64, got $(uname -m)"48[[ "$(id -u)" -ne 0 ]] || die "run as the dedicated gaming user, not root"49for command in cmp diff flock ldd python3 sha256sum systemctl systemd-analyze timeout; do50 command -v "$command" >/dev/null 2>&1 || die "$command is required"51done5253[[ "$payload_dir" == /* ]] || die "payload directory must be absolute"54[[ -d "$payload_dir" && ! -L "$payload_dir" ]] || die "payload directory is unsafe"55[[ "$(stat -Lc '%u' "$payload_dir")" -eq "$(id -u)" ]] || die "payload directory must be owned by the current user"56for required in PACKAGE-SHA256SUMS release-id release/SHA256SUMS; do57 [[ -f "$payload_dir/$required" && ! -L "$payload_dir/$required" ]] \58 || die "$required is missing or unsafe"59done60[[ -z "$(find "$payload_dir" -type l -print -quit)" ]] || die "package payload must not contain symlinks"61if ! diff -u \62 <(printf '%s\n' \63 PACKAGE-SHA256SUMS \64 install-bazzite-package.sh \65 release-id \66 release/LICENSE \67 release/SHA256SUMS \68 release/assets/50-sigil-spark-audio.conf \69 release/assets/70-sigil-remote-input.rules \70 release/assets/72-sigil-uinput.rules \71 release/assets/99-sigil-uinput.rules \72 release/assets/sigil-host.service \73 release/docs/sigil-host-activation.md \74 release/release-manifest.json \75 release/sigil \76 release/sigil-host \77 release/sigil-probe \78 release/tools/rollback-bazzite-release.sh \79 stage-this-release.sh | LC_ALL=C sort) \80 <(find "$payload_dir" -type f -printf '%P\n' | LC_ALL=C sort)81then82 die "package payload does not match the runtime allowlist"83fi84if ! diff -u \85 <(printf '%s\n' \86 LICENSE \87 assets/50-sigil-spark-audio.conf \88 assets/70-sigil-remote-input.rules \89 assets/72-sigil-uinput.rules \90 assets/99-sigil-uinput.rules \91 assets/sigil-host.service \92 docs/sigil-host-activation.md \93 release-manifest.json \94 sigil \95 sigil-host \96 sigil-probe \97 tools/rollback-bazzite-release.sh | LC_ALL=C sort) \98 <(awk '{print $2}' "$payload_dir/release/SHA256SUMS" | LC_ALL=C sort)99then100 die "release checksums do not match the installed-file allowlist"101fi102103(104 cd "$payload_dir"105 sha256sum -c PACKAGE-SHA256SUMS106 cd release107 sha256sum -c SHA256SUMS108)109110release_id="$(tr -d '\n' <"$payload_dir/release-id")"111[[ "$release_id" =~ ^[0-9a-f]{64}$ ]] || die "release-id is not a lowercase SHA-256"112[[ "$(sha256sum "$payload_dir/release/SHA256SUMS" | awk '{print $1}')" == "$release_id" ]] \113 || die "release ID does not bind the release checksums"114115install_root="$HOME/.local/libexec/sigil-spark"116releases_root="$install_root/releases"117release_path="$releases_root/$release_id"118staging_path="$releases_root/.stage-package-$release_id-$$"119current_link="$install_root/current"120previous_link="$install_root/previous"121next_current="$install_root/.current-package-$release_id-$$"122next_previous="$install_root/.previous-package-$release_id-$$"123lock_path="$install_root/.install.lock"124verify_unit="$install_root/.verify-package-$release_id-$$.service"125next_managed_links=()126127# BEGIN release activation transaction helpers128activation_mutated=false129activation_committed=false130activation_reload_service=false131original_current_present=false132original_current_target=""133original_previous_present=false134original_previous_target=""135restore_current_temp="${current_link}.restore-$$"136restore_previous_temp="${previous_link}.restore-$$"137138snapshot_activation_links() {139 if [[ -L "$current_link" ]]; then140 original_current_present=true141 original_current_target="$(readlink "$current_link")"142 else143 original_current_present=false144 original_current_target=""145 fi146 if [[ -L "$previous_link" ]]; then147 original_previous_present=true148 original_previous_target="$(readlink "$previous_link")"149 else150 original_previous_present=false151 original_previous_target=""152 fi153}154155restore_activation_link() {156 local link="$1"157 local present="$2"158 local target="$3"159 local restore_temp="$4"160161 rm -f -- "$restore_temp"162 if [[ "$present" == true ]]; then163 ln -s "$target" "$restore_temp" || return 1164 mv -Tf -- "$restore_temp" "$link" || return 1165 elif [[ -L "$link" ]]; then166 rm -f -- "$link" || return 1167 elif [[ -e "$link" ]]; then168 return 1169 fi170}171172restore_activation_links() {173 if ! $activation_mutated || $activation_committed; then174 return 0175 fi176177 restore_activation_link \178 "$current_link" "$original_current_present" "$original_current_target" \179 "$restore_current_temp" || true180 restore_activation_link \181 "$previous_link" "$original_previous_present" "$original_previous_target" \182 "$restore_previous_temp" || true183 if $activation_reload_service; then184 systemctl --user daemon-reload >/dev/null 2>&1 || true185 fi186}187188activate_release_links() {189 activation_mutated=true190 if [[ -L "$next_previous" ]]; then191 mv -Tf -- "$next_previous" "$previous_link" || return 1192 fi193 mv -Tf -- "$next_current" "$current_link" || return 1194 if $activation_reload_service; then195 systemctl --user daemon-reload || return 1196 fi197 activation_committed=true198}199# END release activation transaction helpers200201ensure_owned_directory() {202 local directory="$1"203 local mode204205 if [[ -e "$directory" || -L "$directory" ]]; then206 [[ -d "$directory" && ! -L "$directory" ]] || die "unsafe install directory: $directory"207 [[ "$(stat -Lc '%u' "$directory")" -eq "$(id -u)" ]] \208 || die "install directory is not owned by the current user: $directory"209 mode="$(stat -Lc '%a' "$directory")"210 (( (8#$mode & 8#022) == 0 )) \211 || die "install directory is group/world writable: $directory"212 else213 install -d -m 0755 "$directory"214 fi215}216217[[ "$HOME" == /* && -d "$HOME" ]] || die "HOME must be an absolute directory"218for directory in "$HOME/.local" "$HOME/.local/libexec" "$install_root" "$releases_root"; do219 ensure_owned_directory "$directory"220done221exec 9>"$lock_path"222flock -x 9223224cleanup() {225 local status=$?226 trap - EXIT INT TERM HUP227 restore_activation_links228 if [[ -d "$staging_path" ]]; then229 find "$staging_path" -type f -delete 2>/dev/null || true230 find "$staging_path" -depth -type d -empty -delete 2>/dev/null || true231 fi232 [[ ! -L "$next_current" ]] || rm -f -- "$next_current"233 [[ ! -L "$next_previous" ]] || rm -f -- "$next_previous"234 [[ ! -L "$restore_current_temp" ]] || rm -f -- "$restore_current_temp"235 [[ ! -L "$restore_previous_temp" ]] || rm -f -- "$restore_previous_temp"236 for next_managed_link in "${next_managed_links[@]}"; do237 [[ ! -L "$next_managed_link" ]] || rm -f -- "$next_managed_link"238 done239 [[ ! -f "$verify_unit" ]] || rm -f -- "$verify_unit"240 exit "$status"241}242trap cleanup EXIT INT TERM HUP243244validate_release_tree() {245 local path="$1"246 local ldd_output247248 [[ -d "$path" && ! -L "$path" ]] || die "release tree is unsafe"249 [[ "$(stat -Lc '%u' "$path")" -eq "$(id -u)" ]] || die "release tree is not owned by the current user"250 (251 cd "$path"252 sha256sum -c SHA256SUMS253 )254 python3 - "$path/release-manifest.json" "$asset_target_contract" <<'PY'255import json256import pathlib257import re258import sys259260path = pathlib.Path(sys.argv[1])261asset_target_contract = sys.argv[2]262try:263 manifest = json.loads(path.read_text())264except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:265 raise SystemExit(f"package install failed: release manifest is invalid: {error}")266267expected = {268 "format": 2,269 "product": "sigil-host",270 "primary_executable": "sigil",271 "compatibility_executable": "sigil-host",272 "target": "x86_64-unknown-linux-gnu.2.17",273 "profile": "release",274 "features": ["in-process-gstreamer"],275 "demo_auth_bypass": False,276 "demo_direct_node": False,277}278for key, value in expected.items():279 if manifest.get(key) != value:280 raise SystemExit(f"package install failed: release manifest field {key} is invalid")281282version = manifest.get("version")283commit = manifest.get("git_commit")284release_tag = manifest.get("release_tag")285asset_name = manifest.get("asset_name")286release_kind = manifest.get("release_kind")287if not isinstance(version, str) or re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z][0-9A-Za-z.-]*)?", version) is None:288 raise SystemExit("package install failed: release manifest version is invalid")289if not isinstance(commit, str) or re.fullmatch(r"[0-9a-f]{40}", commit) is None:290 raise SystemExit("package install failed: release manifest commit is invalid")291if not isinstance(asset_name, str) or re.fullmatch(r"[A-Za-z0-9._-]+[.]tar[.]gz", asset_name) is None:292 raise SystemExit("package install failed: release manifest asset name is invalid")293if release_kind == "product-candidate":294 if release_tag != f"v{version}" or asset_name != f"sigil-{release_tag}-{asset_target_contract}.tar.gz":295 raise SystemExit("package install failed: product release identity is inconsistent")296 if manifest.get("git_dirty") is not False:297 raise SystemExit("package install failed: product release claims dirty source")298 if manifest.get("binary_provenance") != "self-built-clean-head" or manifest.get("binary_provenance_verified") is not True:299 raise SystemExit("package install failed: product binary provenance is invalid")300elif release_kind == "development":301 if release_tag != "development":302 raise SystemExit("package install failed: development release claims a product tag")303else:304 raise SystemExit("package install failed: release kind is invalid")305PY306 for binary in sigil sigil-host sigil-probe; do307 [[ -f "$path/$binary" && ! -L "$path/$binary" && -x "$path/$binary" ]] \308 || die "$binary is not a regular executable"309 mode="$(stat -Lc '%a' "$path/$binary")"310 (( (8#$mode & 8#022) == 0 )) || die "$binary is group/world writable"311 done312 cmp -s "$path/sigil" "$path/sigil-host" \313 || die "sigil-host compatibility executable differs from sigil"314 ldd_output="$(ldd "$path/sigil")"315 printf '%s\n' "$ldd_output"316 grep -q 'not found' <<<"$ldd_output" && die "host binary has an unresolved shared library"317 timeout --signal=TERM --kill-after=2s 5s "$path/sigil" --version318 timeout --signal=TERM --kill-after=2s 5s "$path/sigil-probe" --help >/dev/null319 awk -v executable="$path/sigil" '320 /^ExecStart=/ {321 print "ExecStart=" executable " serve --config=%h/.config/sigil-spark/host.toml"322 next323 }324 { print }325 ' "$path/assets/sigil-host.service" >"$verify_unit"326 chmod 0600 "$verify_unit"327 systemd-analyze --user verify "$verify_unit"328 rm -f -- "$verify_unit"329}330331if [[ -e "$release_path" || -L "$release_path" ]]; then332 [[ -d "$release_path" && ! -L "$release_path" ]] || die "existing release path is unsafe"333 validate_release_tree "$release_path"334else335 install -d -m 0755 "$staging_path"336 (337 cd "$payload_dir/release"338 while IFS= read -r relative; do339 [[ -n "$relative" && "$relative" != /* && "$relative" != *..* ]] \340 || die "release checksum contains an unsafe path"341 install -D -m "$(stat -c '%a' "$relative")" "$relative" "$staging_path/$relative"342 done < <(awk '{print $2}' SHA256SUMS)343 install -m 0644 SHA256SUMS "$staging_path/SHA256SUMS"344 )345 validate_release_tree "$staging_path"346 mv -- "$staging_path" "$release_path"347fi348349current_release_id=""350if [[ -e "$current_link" || -L "$current_link" ]]; then351 [[ -L "$current_link" ]] || die "current activation is not a symlink"352 current_path="$(readlink -f "$current_link")"353 [[ -n "$current_path" && -d "$current_path" ]] || die "current activation is dangling"354 # Fedora Atomic/Bazzite exposes /home through /var/home. Compare canonical355 # paths on both sides so a managed activation remains valid regardless of356 # which equivalent spelling systemd or the login session used for $HOME.357 [[ "$(dirname -- "$current_path")" == "$(readlink -f "$releases_root")" ]] \358 || die "current activation escapes the release root"359 current_release_id="$(basename -- "$current_path")"360fi361if [[ -e "$previous_link" || -L "$previous_link" ]]; then362 [[ -L "$previous_link" ]] || die "previous activation is not a symlink"363fi364snapshot_activation_links365activation_reload_service=true366367config_root="$HOME/.config"368pipewire_dir="$config_root/pipewire/pipewire-pulse.conf.d"369systemd_dir="$config_root/systemd/user"370local_bin="$HOME/.local/bin"371asset_dir="$HOME/.local/share/sigil-spark/package-assets"372install -d -m 0700 "$config_root" "$pipewire_dir" "$systemd_dir" "$local_bin" "$asset_dir"373374# BEGIN managed asset helpers (exercised directly by bazzite-package-assets.sh)375preflight_managed_link() {376 local link="$1"377 local target="$2"378 local release_asset="$3"379380 if [[ -L "$link" ]]; then381 [[ "$(readlink "$link")" == "$target" ]] \382 || die "refusing to replace unmanaged symlink: $link"383 managed_link_action=keep384 elif [[ -e "$link" ]]; then385 [[ -f "$link" && -O "$link" ]] \386 || die "refusing to replace unsafe or unowned path: $link"387 cmp -s "$link" "$release_asset" \388 || die "refusing to replace modified path: $link"389 managed_link_action=replace390 else391 managed_link_action=create392 fi393}394395install_managed_links() {396 local -a actions=()397 local -a prepared_links=()398 local index link next_link399400 [[ "${#managed_links[@]}" -eq 4 \401 && "${#managed_links[@]}" -eq "${#managed_targets[@]}" \402 && "${#managed_links[@]}" -eq "${#managed_release_assets[@]}" ]] \403 || die "internal managed asset list mismatch"404405 # Classify every destination before changing any of them. This prevents a406 # late unmanaged path from leaving an earlier package asset half-migrated.407 for index in "${!managed_links[@]}"; do408 preflight_managed_link \409 "${managed_links[$index]}" \410 "${managed_targets[$index]}" \411 "${managed_release_assets[$index]}"412 actions+=("$managed_link_action")413 done414415 next_managed_links=()416 for index in "${!managed_links[@]}"; do417 [[ "${actions[$index]}" == keep ]] && continue418 link="${managed_links[$index]}"419 next_link="${link}.sigil-package-$release_id-$$-$index"420 [[ ! -e "$next_link" && ! -L "$next_link" ]] \421 || die "temporary managed link path already exists: $next_link"422 prepared_links[index]="$next_link"423 next_managed_links+=("$next_link")424 done425426 for index in "${!managed_links[@]}"; do427 [[ "${actions[$index]}" == keep ]] && continue428 next_link="${prepared_links[$index]}"429 ln -s "${managed_targets[$index]}" "$next_link"430 if mv --help 2>&1 | grep -q -- '--no-target-directory'; then431 mv -Tf -- "$next_link" "${managed_links[$index]}"432 else433 mv -f "$next_link" "${managed_links[$index]}"434 fi435 done436}437# END managed asset helpers438439managed_links=(440 "$pipewire_dir/50-sigil-spark-audio.conf"441 "$systemd_dir/sigil-host.service"442 "$local_bin/sigil-spark-host-rollback"443 "$asset_dir/70-sigil-remote-input.rules"444)445managed_targets=(446 "$install_root/current/assets/50-sigil-spark-audio.conf"447 "$install_root/current/assets/sigil-host.service"448 "$install_root/current/tools/rollback-bazzite-release.sh"449 "$install_root/current/assets/70-sigil-remote-input.rules"450)451managed_release_assets=(452 "$release_path/assets/50-sigil-spark-audio.conf"453 "$release_path/assets/sigil-host.service"454 "$release_path/tools/rollback-bazzite-release.sh"455 "$release_path/assets/70-sigil-remote-input.rules"456)457install_managed_links458459ln -s "releases/$release_id" "$next_current"460if [[ -n "$current_release_id" && "$current_release_id" != "$release_id" ]]; then461 ln -s "releases/$current_release_id" "$next_previous"462fi463activate_release_links464465printf 'package_release=%s\n' "$release_id"466printf 'current=%s\n' "$(readlink -f "$current_link")"467if [[ -L "$previous_link" ]]; then468 printf 'previous=%s\n' "$(readlink -f "$previous_link")"469else470 printf 'previous=none\n'471fi472printf 'package_install=release-and-user-assets-activated\n'473printf 'pipewire_restart=not-attempted\n'474printf 'service_activation=not-attempted\n'475printf 'system_asset=%s\n' "$asset_dir/70-sigil-remote-input.rules"476printf '%s\n' \477 "activation_guide=$install_root/current/docs/sigil-host-activation.md" \478 "next: follow $install_root/current/docs/sigil-host-activation.md as the dedicated gaming user"