Skip to content

Instantly share code, notes, and snippets.

@Kobtul
Created September 16, 2026 03:57
Show Gist options
  • Select an option

  • Save Kobtul/386637452ffa9b0599eb894805e8bb47 to your computer and use it in GitHub Desktop.

Select an option

Save Kobtul/386637452ffa9b0599eb894805e8bb47 to your computer and use it in GitHub Desktop.
arm steam fedora asahi
#!/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