Created
September 16, 2026 03:57
-
-
Save Kobtul/386637452ffa9b0599eb894805e8bb47 to your computer and use it in GitHub Desktop.
arm steam fedora asahi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Install Valve's Steam ARM64 beta and GE-Proton AArch64 on Fedora Asahi Remix. | |
| # Steam and Proton run as ARM64 programs inside muvm; GE-Proton's bundled FEX | |
| # translates x86/x86-64 Windows game code. | |
| set -Eeuo pipefail | |
| readonly STEAM_MANIFEST_URL="https://client-update.steamstatic.com/steam_client_publicbeta_linuxarm64" | |
| readonly STEAM_CDN_URL="https://client-update.steamstatic.com" | |
| readonly GE_RELEASE_API="https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases/latest" | |
| readonly STEAM_RUNTIME_APPID="4185400" | |
| STEAMROOT="${STEAMROOT:-$HOME/.local/share/Steam}" | |
| DOWNLOAD_DIR="${DOWNLOAD_DIR:-$HOME/Downloads}" | |
| LAUNCH_AFTER_INSTALL=1 | |
| usage() { | |
| printf '%s\n' \ | |
| "Usage: ${0##*/} [--no-launch]" \ | |
| '' \ | |
| ' --no-launch Install everything without starting Steam afterward.' \ | |
| ' -h, --help Show this help.' | |
| } | |
| log() { | |
| printf '\n==> %s\n' "$*" | |
| } | |
| die() { | |
| printf 'ERROR: %s\n' "$*" >&2 | |
| exit 1 | |
| } | |
| command_required() { | |
| command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1" | |
| } | |
| safe_symlink() { | |
| local target=$1 | |
| local link=$2 | |
| if [[ -e "$link" && ! -L "$link" ]]; then | |
| die "$link already exists and is not a symbolic link; move it aside and rerun." | |
| fi | |
| ln -sfnT "$target" "$link" | |
| } | |
| download() { | |
| local url=$1 | |
| local destination=$2 | |
| local temporary="${destination}.part" | |
| curl --fail --location --retry 3 --retry-delay 2 \ | |
| --output "$temporary" "$url" | |
| mv -f "$temporary" "$destination" | |
| } | |
| while (($#)); do | |
| case "$1" in | |
| --no-launch) | |
| LAUNCH_AFTER_INSTALL=0 | |
| ;; | |
| -h|--help) | |
| usage | |
| exit 0 | |
| ;; | |
| *) | |
| usage >&2 | |
| die "Unknown option: $1" | |
| ;; | |
| esac | |
| shift | |
| done | |
| [[ $(uname -m) == aarch64 ]] || die 'This installer requires an AArch64 system.' | |
| [[ -r /etc/os-release ]] || die 'Cannot identify this Linux distribution.' | |
| # shellcheck source=/dev/null | |
| source /etc/os-release | |
| [[ ${ID:-} == fedora-asahi-remix ]] || die \ | |
| "Expected Fedora Asahi Remix, found ${PRETTY_NAME:-an unknown distribution}." | |
| if [[ $(getconf PAGESIZE) != 16384 ]]; then | |
| printf 'WARNING: host page size is %s, not the expected 16384 bytes.\n' \ | |
| "$(getconf PAGESIZE)" >&2 | |
| fi | |
| if [[ $(uname -r) == 7.1.5*asahi* ]]; then | |
| die 'Asahi kernel 7.1.5 has a known muvm regression. Update to 7.1.6 or newer and reboot first.' | |
| fi | |
| command_required sudo | |
| command_required dnf | |
| if rpm -q steam >/dev/null 2>&1; then | |
| printf '%s\n' \ | |
| "WARNING: Fedora's steam wrapper is already installed." \ | |
| 'This script will not remove or use it; launch Steam with steam-arm64-asahi.' >&2 | |
| fi | |
| log 'Installing Fedora dependencies (not the Fedora steam package)' | |
| sudo dnf install -y \ | |
| muvm \ | |
| fex-emu \ | |
| gtk2 \ | |
| ibus \ | |
| libvdpau \ | |
| NetworkManager-libnm \ | |
| dbus-glib \ | |
| dbus-x11 \ | |
| python3 \ | |
| curl \ | |
| unzip \ | |
| tar | |
| for required_command in curl unzip tar sha256sum sha512sum awk sed find sort python3; do | |
| command_required "$required_command" | |
| done | |
| mkdir -p "$DOWNLOAD_DIR" "$STEAMROOT" | |
| log 'Resolving the current Valve Steam ARM64 beta' | |
| steam_manifest=$(curl --fail --silent --show-error --location \ | |
| "$STEAM_MANIFEST_URL") | |
| mapfile -t steam_metadata < <( | |
| printf '%s\n' "$steam_manifest" | python3 -c ' | |
| import re | |
| import sys | |
| manifest = sys.stdin.read() | |
| sections = ( | |
| "bins_linuxarm64_linuxarm64", | |
| "codecs_linuxarm64_linuxarm64", | |
| "sdl3_linuxarm64_linuxarm64", | |
| ) | |
| for section in sections: | |
| match = re.search(rf"\"{section}\"\s*\{{(.*?)\n\s*\}}", manifest, re.S) | |
| if not match: | |
| raise SystemExit(f"Valve manifest is missing {section}") | |
| file_match = re.search(r"\"file\"\s+\"([^\"]+)\"", match.group(1)) | |
| sha_match = re.search(r"\"sha2\"\s+\"([0-9a-f]{64})\"", match.group(1)) | |
| if not file_match or not sha_match: | |
| raise SystemExit(f"Valve manifest has incomplete metadata for {section}") | |
| print(file_match.group(1)) | |
| print(sha_match.group(1)) | |
| ' | |
| ) | |
| steam_zip_name=${steam_metadata[0]:-} | |
| steam_zip_sha256=${steam_metadata[1]:-} | |
| codec_zip_name=${steam_metadata[2]:-} | |
| codec_zip_sha256=${steam_metadata[3]:-} | |
| sdl_zip_name=${steam_metadata[4]:-} | |
| sdl_zip_sha256=${steam_metadata[5]:-} | |
| [[ $steam_zip_name == bins_linuxarm64_linuxarm64.zip.* ]] || \ | |
| die 'Valve manifest did not contain the expected ARM64 client archive.' | |
| [[ $steam_zip_sha256 =~ ^[[:xdigit:]]{64}$ ]] || \ | |
| die 'Valve manifest did not contain a valid ARM64 client SHA-256.' | |
| [[ $codec_zip_name == codecs_linuxarm64_linuxarm64.zip.* ]] || \ | |
| die 'Valve manifest did not contain the expected ARM64 codec archive.' | |
| [[ $codec_zip_sha256 =~ ^[[:xdigit:]]{64}$ ]] || \ | |
| die 'Valve manifest did not contain a valid ARM64 codec SHA-256.' | |
| [[ $sdl_zip_name == sdl3_linuxarm64_linuxarm64.zip.* ]] || \ | |
| die 'Valve manifest did not contain the expected ARM64 SDL3 archive.' | |
| [[ $sdl_zip_sha256 =~ ^[[:xdigit:]]{64}$ ]] || \ | |
| die 'Valve manifest did not contain a valid ARM64 SDL3 SHA-256.' | |
| steam_zip="$DOWNLOAD_DIR/bins_linuxarm64_linuxarm64.zip" | |
| codec_zip="$DOWNLOAD_DIR/codecs_linuxarm64_linuxarm64.zip" | |
| sdl_zip="$DOWNLOAD_DIR/sdl3_linuxarm64_linuxarm64.zip" | |
| log "Downloading $steam_zip_name" | |
| download "$STEAM_CDN_URL/$steam_zip_name" "$steam_zip" | |
| printf '%s %s\n' "$steam_zip_sha256" "$steam_zip" | sha256sum --check - | |
| log "Downloading $codec_zip_name" | |
| download "$STEAM_CDN_URL/$codec_zip_name" "$codec_zip" | |
| printf '%s %s\n' "$codec_zip_sha256" "$codec_zip" | sha256sum --check - | |
| log "Downloading $sdl_zip_name" | |
| download "$STEAM_CDN_URL/$sdl_zip_name" "$sdl_zip" | |
| printf '%s %s\n' "$sdl_zip_sha256" "$sdl_zip" | sha256sum --check - | |
| log "Extracting Steam ARM64 to $STEAMROOT" | |
| unzip -o -q "$steam_zip" -d "$STEAMROOT" | |
| unzip -o -q "$codec_zip" -d "$STEAMROOT" | |
| unzip -o -q "$sdl_zip" -d "$STEAMROOT" | |
| [[ -x "$STEAMROOT/steamrtarm64/steam" || \ | |
| -f "$STEAMROOT/steamrtarm64/steam" ]] || \ | |
| die 'The Valve archive did not create steamrtarm64/steam.' | |
| mkdir -p "$STEAMROOT/package" "$HOME/.steam" | |
| printf '%s\n' publicbeta > "$STEAMROOT/package/beta" | |
| chmod -R u+rwX "$STEAMROOT/steamrtarm64" | |
| for executable in \ | |
| "$STEAMROOT/steamrtarm64/steam" \ | |
| "$STEAMROOT/steamrtarm64/steamwebhelper" \ | |
| "$STEAMROOT/steamrtarm64/steamwebhelper.sh" \ | |
| "$STEAMROOT/steamrtarm64/gldriverquery" \ | |
| "$STEAMROOT/steamrtarm64/vulkandriverquery" \ | |
| "$STEAMROOT/steamrtarm64/steamsysinfo" \ | |
| "$STEAMROOT/steam.sh"; do | |
| [[ -e "$executable" ]] && chmod u+x "$executable" | |
| done | |
| safe_symlink "$STEAMROOT/steamrtarm64" "$STEAMROOT/steamrt64" | |
| touch "$STEAMROOT/.steam-enable-steamrt64-client" | |
| safe_symlink "$STEAMROOT" "$HOME/.steam/steam" | |
| safe_symlink "$STEAMROOT" "$HOME/.steam/root" | |
| safe_symlink "$STEAMROOT/linuxarm64" "$HOME/.steam/sdkarm64" | |
| log 'Resolving the latest GE-Proton AArch64 release' | |
| ge_release_json=$(curl --fail --silent --show-error --location "$GE_RELEASE_API") | |
| mapfile -t ge_asset_urls < <( | |
| printf '%s\n' "$ge_release_json" | python3 -c ' | |
| import json | |
| import sys | |
| release = json.load(sys.stdin) | |
| assets = [asset["browser_download_url"] for asset in release.get("assets", [])] | |
| archives = [url for url in assets if url.endswith("-aarch64.tar.gz")] | |
| checksums = [url for url in assets if url.endswith("-aarch64.sha512sum")] | |
| if len(archives) != 1 or len(checksums) != 1: | |
| raise SystemExit("Expected exactly one AArch64 archive and checksum") | |
| print(archives[0]) | |
| print(checksums[0]) | |
| ' | |
| ) | |
| ge_arm_url=${ge_asset_urls[0]:-} | |
| ge_sum_url=${ge_asset_urls[1]:-} | |
| [[ $ge_arm_url == https://github.com/GloriousEggroll/proton-ge-custom/releases/*-aarch64.tar.gz ]] || \ | |
| die 'GitHub did not return the expected GE-Proton AArch64 archive URL.' | |
| [[ $ge_sum_url == https://github.com/GloriousEggroll/proton-ge-custom/releases/*-aarch64.sha512sum ]] || \ | |
| die 'GitHub did not return the expected GE-Proton AArch64 checksum URL.' | |
| ge_archive="$DOWNLOAD_DIR/${ge_arm_url##*/}" | |
| ge_sum_file="$DOWNLOAD_DIR/${ge_sum_url##*/}" | |
| log "Downloading ${ge_archive##*/}" | |
| download "$ge_arm_url" "$ge_archive" | |
| download "$ge_sum_url" "$ge_sum_file" | |
| ( | |
| cd "$DOWNLOAD_DIR" | |
| sha512sum --check "${ge_sum_file##*/}" | |
| ) | |
| if tar -tzf "$ge_archive" | grep -E '(^/|(^|/)\.\.(/|$))' >/dev/null; then | |
| die 'GE-Proton archive contains an unsafe absolute or parent-directory path.' | |
| fi | |
| ge_arm_dir=$(tar -tzf "$ge_archive" | sed -n '1s#/.*##p') | |
| [[ $ge_arm_dir == GE-Proton*-aarch64 ]] || \ | |
| die 'GE-Proton archive has an unexpected top-level directory.' | |
| log "Installing $ge_arm_dir" | |
| mkdir -p "$STEAMROOT/compatibilitytools.d" | |
| tar -xzf "$ge_archive" -C "$STEAMROOT/compatibilitytools.d" | |
| ge_tool_manifest="$STEAMROOT/compatibilitytools.d/$ge_arm_dir/toolmanifest.vdf" | |
| [[ -f "$ge_tool_manifest" ]] || die 'GE-Proton toolmanifest.vdf is missing.' | |
| # Work around the ARM compatibility dependency checker, matching Ubuntu Asahi's | |
| # current installer. Runtime 4.0 is still explicitly requested at first launch. | |
| sed -i "s#\"$STEAM_RUNTIME_APPID\"#\"\"#g" "$ge_tool_manifest" | |
| log 'Creating the Steam ARM64 launcher' | |
| mkdir -p "$HOME/.local/bin" "$HOME/.local/share/applications" | |
| launcher="$HOME/.local/bin/steam-arm64-asahi" | |
| desktop_file="$HOME/.local/share/applications/steam-arm64-asahi.desktop" | |
| install -m 0755 /dev/stdin "$launcher" <<'LAUNCHER' | |
| #!/usr/bin/env bash | |
| set -e | |
| STEAMROOT="${STEAMROOT:-$HOME/.local/share/Steam}" | |
| extra_flags=() | |
| patch_waiting_for_network() { | |
| python3 - "$STEAMROOT" <<'PYTHON' | |
| from pathlib import Path | |
| import os | |
| import shutil | |
| import sys | |
| steam_root = Path(sys.argv[1]) | |
| chunks = sorted((steam_root / "steamui").glob("chunk~*.js")) | |
| if not chunks: | |
| raise SystemExit( | |
| "Steam UI files do not exist yet. Run Steam normally once, close it " | |
| "if it sticks at 'Waiting for network', then rerun this fix." | |
| ) | |
| replacements = ( | |
| ( | |
| b'const t=(0,B.Dp)("System.Network.RegisterForDeviceChanges");', | |
| b'const t="function"==typeof SteamClient?.System?.Network?.RegisterForDeviceChanges;', | |
| ), | |
| ( | |
| b'(0,B.Dp)("System.Network.GetProxyInfo")&&SteamClient.System.Network.GetProxyInfo().then', | |
| b'"function"==typeof SteamClient?.System?.Network?.GetProxyInfo&&SteamClient.System.Network.GetProxyInfo().then', | |
| ), | |
| ( | |
| b'(0,B.Dp)("System.Network.RegisterForConnectivityTestChanges")&&SteamClient.System.Network.RegisterForConnectivityTestChanges(this.OnConnectivityTestStateChanged)', | |
| b'"function"==typeof SteamClient?.System?.Network?.RegisterForConnectivityTestChanges&&SteamClient.System.Network.RegisterForConnectivityTestChanges(this.OnConnectivityTestStateChanged)', | |
| ), | |
| ( | |
| b't||(this.m_bIsAwaitingInitialNetworkState=!1)', | |
| b't||(this.m_bIsAwaitingInitialNetworkState=!1,this.m_bIsConnectedToANetwork=!0)', | |
| ), | |
| ) | |
| already_patched = [] | |
| patchable = [] | |
| for chunk in chunks: | |
| data = chunk.read_bytes() | |
| old_counts = [data.count(old) for old, _ in replacements] | |
| new_counts = [data.count(new) for _, new in replacements] | |
| if old_counts == [1, 1, 1, 1]: | |
| patchable.append((chunk, data)) | |
| elif new_counts == [1, 1, 1, 1]: | |
| already_patched.append(chunk) | |
| if already_patched and not patchable: | |
| print(f"Steam login workaround is already applied to {already_patched[0]}") | |
| raise SystemExit(0) | |
| if len(patchable) != 1: | |
| raise SystemExit( | |
| "Refusing to patch: expected exactly one Steam UI chunk with the known " | |
| f"patterns, found {len(patchable)}. Valve may have changed the client." | |
| ) | |
| chunk, data = patchable[0] | |
| backup = chunk.with_name(chunk.name + ".asahi-login-fix-backup") | |
| if not backup.exists(): | |
| shutil.copy2(chunk, backup) | |
| for old, new in replacements: | |
| data = data.replace(old, new, 1) | |
| temporary = chunk.with_name(chunk.name + ".asahi-login-fix-tmp") | |
| temporary.write_bytes(data) | |
| os.chmod(temporary, chunk.stat().st_mode) | |
| os.replace(temporary, chunk) | |
| print(f"Applied one-time Steam login workaround to {chunk}") | |
| print(f"Original saved as {backup}") | |
| PYTHON | |
| } | |
| if [[ ${1:-} == --fix-waiting-for-network ]]; then | |
| shift | |
| patch_waiting_for_network | |
| extra_flags+=( | |
| -nobootstrapupdate | |
| -skipinitialbootstrap | |
| -norepairfiles | |
| ) | |
| fi | |
| restart_count=0 | |
| while true; do | |
| if muvm \ | |
| --env="LD_LIBRARY_PATH=$STEAMROOT/steamrtarm64:${LD_LIBRARY_PATH:-}" \ | |
| "$STEAMROOT/steamrtarm64/steam" \ | |
| -noverifyfiles \ | |
| "${extra_flags[@]}" \ | |
| "$@"; then | |
| status=0 | |
| else | |
| status=$? | |
| fi | |
| if (( status != 42 )); then | |
| exit "$status" | |
| fi | |
| ((restart_count += 1)) | |
| if (( restart_count > 5 )); then | |
| printf 'Steam requested more than five consecutive restarts; stopping.\n' >&2 | |
| exit 42 | |
| fi | |
| printf 'Steam update completed; restarting inside muvm...\n' | |
| done | |
| LAUNCHER | |
| printf '%s\n' \ | |
| '[Desktop Entry]' \ | |
| 'Type=Application' \ | |
| 'Name=Steam ARM64 (muvm)' \ | |
| 'Comment=Valve Steam ARM64 beta with ARM64 Proton on Asahi Linux' \ | |
| "Exec=$launcher" \ | |
| 'Icon=steam' \ | |
| 'Terminal=false' \ | |
| 'Categories=Game;' \ | |
| > "$desktop_file" | |
| if command -v update-desktop-database >/dev/null 2>&1; then | |
| update-desktop-database "$HOME/.local/share/applications" || true | |
| fi | |
| printf '\nInstalled successfully:\n' | |
| printf ' Steam: %s\n' "$STEAMROOT/steamrtarm64/steam" | |
| printf ' Proton: %s\n' "$STEAMROOT/compatibilitytools.d/$ge_arm_dir" | |
| printf ' Launcher: %s\n' "$launcher" | |
| printf ' Desktop: %s\n' "$desktop_file" | |
| printf "\nIn Steam, force %s in each game's Compatibility settings.\n" "$ge_arm_dir" | |
| printf '%s\n' \ | |
| '' \ | |
| 'If a fresh login sticks at “Waiting for network”, close Steam and run:' \ | |
| " $launcher --fix-waiting-for-network" \ | |
| 'Use that mode only for the initial login; launch normally afterward.' \ | |
| 'If Runtime 4.0 was not queued after login, run:' \ | |
| " $launcher steam://install/$STEAM_RUNTIME_APPID" | |
| if ((LAUNCH_AFTER_INSTALL)); then | |
| if [[ ! -e "$STEAMROOT/steamapps/appmanifest_${STEAM_RUNTIME_APPID}.acf" ]]; then | |
| log 'Starting Steam and requesting Steam Linux Runtime 4.0 ARM64' | |
| exec "$launcher" "steam://install/$STEAM_RUNTIME_APPID" | |
| fi | |
| log 'Starting Steam ARM64 inside muvm' | |
| exec "$launcher" | |
| fi | |
| printf '\nRun Steam later with:\n %q\n' "$launcher" | |
| printf 'Install Runtime 4.0 if needed with:\n %q %q\n' \ | |
| "$launcher" "steam://install/$STEAM_RUNTIME_APPID" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment