mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
fix: fall back to Valve's stable Steam client when the beta one keeps failing
The native arm64 client follows steamdeck_publicbeta, so a Valve beta regression reaches every user at once. If the repaired beta client is still started twice without being shown, or the beta manifest carries no arm64 client, droiddeck-steam-install now installs steamdeck_stable, keeps it, and logs why (listing the manifest's blocks in the second case). The session asks the installer for the channel, so -clientbeta and package/beta stay in sync. BL_STEAM_CHANNEL or ~/.config/droiddeck/steam-channel (beta|stable) pins it. Valve's 2026-10-15 beta moved the x86 client into a Steam Runtime container. In case the arm64 client follows, its session now exports STEAM_STEAMRT_RECURSION_GUARD=1 and, only if steamrtarm64 carries pv-runtime or a steam-runtime-check-requirements probe, starts it through the same steamrt shim the x86-64 client uses (now droiddeck-steamrt-shim) and logs it.
This commit is contained in:
1 parent
b88eebe09b
commit
7aeb7779bb
9 files changed
+379
-65
No files matched your search
@@ -78,6 +78,7 @@ object SessionFiles {
|
||||
"usr/local/bin/droiddeck-steam-games" to "usr/local/bin/droiddeck-steam-games",
|
||||
"usr/local/bin/droiddeck-pad-defaults" to "usr/local/bin/droiddeck-pad-defaults",
|
||||
"usr/local/bin/droiddeck-steam-x64" to "usr/local/bin/droiddeck-steam-x64",
|
||||
"usr/local/bin/droiddeck-steamrt-shim" to "usr/local/bin/droiddeck-steamrt-shim",
|
||||
"usr/local/bin/droiddeck-x64-arm-proton" to "usr/local/bin/droiddeck-x64-arm-proton",
|
||||
"usr/local/bin/droiddeck-x64-arm-webhelper" to "usr/local/bin/droiddeck-x64-arm-webhelper",
|
||||
"usr/local/bin/droiddeck-x64-webhelper" to "usr/local/bin/droiddeck-x64-webhelper",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Native arm64 Steam client: channel and runtime
|
||||
|
||||
The default client is Valve's arm64 Linux build (`steamrtarm64/steam`). Valve does not support it, but
|
||||
publishes it on the Steam Deck client channels. `droiddeck-steam-install` installs it and
|
||||
`droiddeck-session` starts it with `STEAM_RUNTIME=1`.
|
||||
|
||||
## Channel
|
||||
|
||||
- The default is `steamdeck_publicbeta` (manifest `steam_client_steamdeck_publicbeta_linuxarm64`).
|
||||
Valve also publishes `steam_client_steamdeck_stable_linuxarm64`.
|
||||
- `droiddeck-steam-install --channel` prints the channel in use. The session passes that value as
|
||||
`-clientbeta` and writes it to `package/beta`. The two must match, or the client loops on rc 42.
|
||||
- Setting: `BL_STEAM_CHANNEL`, otherwise `~/.config/droiddeck/steam-channel`, set to `beta` or
|
||||
`stable`. A set channel never changes on its own.
|
||||
- With no setting, the client falls back to `steamdeck_stable` automatically. It stays on stable
|
||||
(`package/droiddeck-channel`) until a setting says otherwise. The fallback happens:
|
||||
- when the client was started two more times without being shown after the start-failure repair
|
||||
(`package/droiddeck-unconfirmed`: two unconfirmed starts trigger a repair, which writes
|
||||
`repaired`; two more unconfirmed starts trigger the fallback);
|
||||
- when the beta manifest has no `*_linuxarm64_linuxarm64` component. The install log then lists the
|
||||
manifest's block names, so a renamed or removed arm64 build can be seen.
|
||||
- The x86-64 client keeps `steamdeck_publicbeta` (see [x86-64-steam-client.md](x86-64-steam-client.md)).
|
||||
|
||||
## Steam Runtime container
|
||||
|
||||
Valve's 2026-10-15 beta made the x86 Linux client 64-bit and put it in a Steam Runtime container
|
||||
(pressure-vessel). The container needs unprivileged user and mount namespaces and a D-Bus session bus,
|
||||
which an Android app does not have. The x86-64 path already works around this. The arm64 client gets the
|
||||
same treatment if Valve containerizes it too:
|
||||
|
||||
- `STEAM_STEAMRT_RECURSION_GUARD=1` is exported for both clients. It tells the client that it is
|
||||
already inside its container, so after an update it does not re-execute itself through
|
||||
`pressure-vessel-wrap` (bwrap).
|
||||
- `droiddeck-steamrt-shim <client dir> <shim dir>` builds the shim when the client carries
|
||||
`pv-runtime/steam-runtime-steamrt` or a `steam-runtime-check-requirements` probe:
|
||||
- Valve's runtime is linked file by file;
|
||||
- the probe is replaced with `exit 0`;
|
||||
- its own `steam-runtime-launch-client` runs commands directly.
|
||||
|
||||
For the arm64 client the session then starts it with `STEAM_RUNTIME_STEAMRT` and `PATH` pointing
|
||||
at `~/.droiddeck-steamrt-shim` and logs "Valve containerized the arm64 Steam client". Without those
|
||||
files the shim is not built and the start is unchanged.
|
||||
@@ -53,7 +53,8 @@ arm64 client down first.
|
||||
("Steam now requires user namespaces") when `steam-runtime-check-requirements`' bwrap probe cannot
|
||||
create a user namespace, which no Android app can. `STEAM_RUNTIME_STEAMRT` points at
|
||||
`steamrt-shim`: Valve's runtime linked file by file with only the probe replaced (`exit 0`). proot
|
||||
is the container, as for the arm64 client.
|
||||
is the container, as for the arm64 client. `droiddeck-steamrt-shim` builds it, and builds the same
|
||||
shim for the arm64 client if that one is containerized too ([arm64-steam-client.md](arm64-steam-client.md)).
|
||||
- **Re-exec after an update.** The client re-executes itself inside the SteamRT container through
|
||||
`pressure-vessel-wrap` (bwrap). `STEAM_STEAMRT_RECURSION_GUARD=1` says it is already inside one; the
|
||||
FEX rootfs with the client's `steamrt64` libraries is that container.
|
||||
|
||||
@@ -403,6 +403,11 @@ case $mode in
|
||||
droiddeck-steam-x64 --sync-login || true
|
||||
fi
|
||||
steam_client=("$steam_root/steamrtarm64/steam")
|
||||
_bl_srt_shim=$HOME/.droiddeck-steamrt-shim
|
||||
if droiddeck-steamrt-shim "$steam_root/steamrtarm64" "$_bl_srt_shim"; then
|
||||
echo "== steam: Valve containerized the arm64 Steam client (it carries a Steam Runtime container); starting it through $_bl_srt_shim"
|
||||
steam_client=(env STEAM_RUNTIME_STEAMRT="$_bl_srt_shim" PATH="$_bl_srt_shim/bin:$PATH" "${steam_client[@]}")
|
||||
fi
|
||||
fi
|
||||
_bl_proton_root=$steam_root
|
||||
if [ "$steam_x64" = 1 ]; then
|
||||
@@ -746,10 +751,13 @@ BUSCONF
|
||||
fi
|
||||
|
||||
# The UI bridge launches SteamVR, which needs the guest agent's library environment.
|
||||
_bl_compat_env=(env -u LD_LIBRARY_PATH -u STEAM_RUNTIME)
|
||||
_bl_compat_env=(env -u LD_LIBRARY_PATH -u STEAM_RUNTIME -u STEAM_STEAMRT_RECURSION_GUARD)
|
||||
[ "${LD_LIBRARY_PATH+x}" ] && _bl_compat_env+=("LD_LIBRARY_PATH=$LD_LIBRARY_PATH")
|
||||
[ "${STEAM_RUNTIME+x}" ] && _bl_compat_env+=("STEAM_RUNTIME=$STEAM_RUNTIME")
|
||||
[ "${STEAM_STEAMRT_RECURSION_GUARD+x}" ] && _bl_compat_env+=("STEAM_STEAMRT_RECURSION_GUARD=$STEAM_STEAMRT_RECURSION_GUARD")
|
||||
export STEAM_RUNTIME=1
|
||||
# A containerized client re-executes itself through pressure-vessel-wrap (bwrap) unless told it is already inside.
|
||||
export STEAM_STEAMRT_RECURSION_GUARD=1
|
||||
export LD_LIBRARY_PATH=$steam_root/steamrtarm64:$steam_root/steamrtarm64/panorama${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
|
||||
export SDL_VIDEO_X11_DGAMOUSE=0
|
||||
decky_loader=$HOME/homebrew/services/PluginLoader
|
||||
@@ -829,6 +837,9 @@ BUSCONF
|
||||
unset _stale _f
|
||||
fi
|
||||
steam_channel=steamdeck_publicbeta
|
||||
if [ "$steam_x64" = 0 ]; then
|
||||
steam_channel=$(droiddeck-steam-install --channel 2>/dev/null || echo steamdeck_publicbeta)
|
||||
fi
|
||||
_bl_lang=()
|
||||
[ -n "${BL_STEAM_LANGUAGE:-}" ] && _bl_lang=(-language "$BL_STEAM_LANGUAGE")
|
||||
steam_beta=$steam_root/package/beta
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
cdn_list="https://client-update.steamstatic.com https://client-update.akamai.steamstatic.com https://client-update.fastly.steamstatic.com"
|
||||
steam_channel=steamdeck_publicbeta
|
||||
manifest=steam_client_${steam_channel}_linuxarm64
|
||||
beta_channel=steamdeck_publicbeta
|
||||
stable_channel=steamdeck_stable
|
||||
steam_root=${XDG_DATA_HOME:-$HOME/.local/share}/Steam
|
||||
channel_state=$steam_root/package/droiddeck-channel
|
||||
pinned=1
|
||||
wanted=${BL_STEAM_CHANNEL:-$(cat "$HOME/.config/droiddeck/steam-channel" 2>/dev/null || true)}
|
||||
case $wanted in
|
||||
stable|"$stable_channel") steam_channel=$stable_channel ;;
|
||||
beta|"$beta_channel") steam_channel=$beta_channel ;;
|
||||
*)
|
||||
pinned=
|
||||
steam_channel=$beta_channel
|
||||
[ "$(cat "$channel_state" 2>/dev/null)" = "$stable_channel" ] && steam_channel=$stable_channel
|
||||
;;
|
||||
esac
|
||||
if [ "${1:-}" = --channel ]; then
|
||||
echo "$steam_channel"
|
||||
exit 0
|
||||
fi
|
||||
manifest=steam_client_${steam_channel}_linuxarm64
|
||||
stamp=$steam_root/package/droiddeck-installed
|
||||
legacy_stamp=$steam_root/package/bannerlator-installed
|
||||
unconfirmed=$steam_root/package/droiddeck-unconfirmed
|
||||
@@ -12,11 +29,21 @@ if [ -f "$legacy_stamp" ] && [ ! -f "$stamp" ]; then
|
||||
mv -f "$legacy_stamp" "$stamp" || true
|
||||
fi
|
||||
repair=""
|
||||
fallback=""
|
||||
starts=0
|
||||
[ -f "$unconfirmed" ] && starts=$(grep -cx start "$unconfirmed" || true)
|
||||
if [ -e "$repair_request" ]; then
|
||||
repair="requested"
|
||||
elif [ -f "$unconfirmed" ] && ! grep -qx repaired "$unconfirmed" \
|
||||
&& [ "$(grep -cx start "$unconfirmed")" -ge 2 ]; then
|
||||
repair="the client was started $(grep -cx start "$unconfirmed") times without showing anything"
|
||||
elif [ "$starts" -ge 2 ] && ! grep -qx repaired "$unconfirmed"; then
|
||||
repair="the client was started $starts times without showing anything"
|
||||
elif [ "$starts" -ge 2 ] && [ -z "$pinned" ] && [ "$steam_channel" = "$beta_channel" ] && [ "${1:-}" != --sync ]; then
|
||||
repair="the repaired $beta_channel client was started $starts more times without showing anything"
|
||||
fallback=1
|
||||
fi
|
||||
if [ -n "$fallback" ]; then
|
||||
echo "== STEP $(date -u +%H:%M:%S) falling back to Valve's $stable_channel Steam client: $repair"
|
||||
steam_channel=$stable_channel
|
||||
manifest=steam_client_${steam_channel}_linuxarm64
|
||||
fi
|
||||
if [ -n "$repair" ]; then
|
||||
echo "== STEP $(date -u +%H:%M:%S) repairing the Steam client ($repair)"
|
||||
@@ -42,20 +69,42 @@ fi
|
||||
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
cdn=""
|
||||
for candidate in $cdn_list; do
|
||||
if curl -fsSL --connect-timeout 10 --max-time 30 --retry 2 --retry-connrefused --retry-delay 3 \
|
||||
"$candidate/$manifest" -o "$work/manifest"; then
|
||||
cdn=$candidate
|
||||
break
|
||||
fetch_manifest() {
|
||||
cdn=""
|
||||
for candidate in $cdn_list; do
|
||||
if curl -fsSL --connect-timeout 10 --max-time 30 --retry 2 --retry-connrefused --retry-delay 3 \
|
||||
"$candidate/$manifest" -o "$work/manifest"; then
|
||||
cdn=$candidate
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ -n "$cdn" ] || { echo "droiddeck-steam-install: no Steam client CDN answered for $manifest" >&2; exit 1; }
|
||||
cdn_order=("$cdn")
|
||||
for candidate in $cdn_list; do
|
||||
[ "$candidate" = "$cdn" ] || cdn_order+=("$candidate")
|
||||
done
|
||||
version=$(awk -F'"' '$2 == "version" { print $4; exit }' "$work/manifest")
|
||||
awk -F'"' '
|
||||
/^\t"/ && NF >= 3 && $4 == "" { block = $2 }
|
||||
$2 == "file" { file[block] = $4 }
|
||||
$2 == "sha2" { sha[block] = $4 }
|
||||
END { for (b in file) if (b ~ /_all$/ || b ~ /_linuxarm64_linuxarm64$/) print b, file[b], sha[b] }
|
||||
' "$work/manifest" | sort > "$work/parts"
|
||||
[ -s "$work/parts" ] && return 0
|
||||
echo "droiddeck-steam-install: no client components in $manifest; its blocks are:" \
|
||||
"$(awk -F'"' '/^\t"/ && NF >= 3 && $4 == "" { printf "%s ", $2 }' "$work/manifest")" >&2
|
||||
return 1
|
||||
}
|
||||
if ! fetch_manifest; then
|
||||
if [ -n "$sync" ] || [ -n "$pinned" ] || [ "$steam_channel" = "$stable_channel" ]; then
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
[ -n "$cdn" ] || { echo "droiddeck-steam-install: no Steam client CDN answered for $manifest" >&2; exit 1; }
|
||||
cdn_order=("$cdn")
|
||||
for candidate in $cdn_list; do
|
||||
[ "$candidate" = "$cdn" ] || cdn_order+=("$candidate")
|
||||
done
|
||||
version=$(awk -F'"' '$2 == "version" { print $4; exit }' "$work/manifest")
|
||||
echo "== STEP $(date -u +%H:%M:%S) falling back to Valve's $stable_channel Steam client: $manifest has no arm64 client"
|
||||
steam_channel=$stable_channel
|
||||
manifest=steam_client_${steam_channel}_linuxarm64
|
||||
fallback=1
|
||||
fetch_manifest || exit 1
|
||||
fi
|
||||
if [ -n "$sync" ]; then
|
||||
installed=$(awk -F'"' '$2 == "version" { print $4; exit }' "$steam_root/package/$manifest.manifest" 2>/dev/null || true)
|
||||
if [ -x "$steam_root/steamrtarm64/steam" ] && [ "$installed" = "$version" ]; then
|
||||
@@ -64,13 +113,6 @@ if [ -n "$sync" ]; then
|
||||
fi
|
||||
echo "== STEP $(date -u +%H:%M:%S) updating the arm64 Steam client to $version (was ${installed:-none}) for the x86-64 client"
|
||||
fi
|
||||
awk -F'"' '
|
||||
/^\t"/ && NF >= 3 && $4 == "" { block = $2 }
|
||||
$2 == "file" { file[block] = $4 }
|
||||
$2 == "sha2" { sha[block] = $4 }
|
||||
END { for (b in file) if (b ~ /_all$/ || b ~ /_linuxarm64_linuxarm64$/) print b, file[b], sha[b] }
|
||||
' "$work/manifest" | sort > "$work/parts"
|
||||
[ -s "$work/parts" ] || { echo "droiddeck-steam-install: no client components in $manifest" >&2; exit 1; }
|
||||
total=$(grep -c . "$work/parts" 2>/dev/null || echo 0)
|
||||
|
||||
mkdir -p "$steam_root/package"
|
||||
@@ -117,6 +159,10 @@ ln -sfn "$steam_root/linuxarm64" "$HOME/.steam/sdkarm64"
|
||||
ln -sfn "$steam_root/steamrtarm64" "$HOME/.steam/binarm64"
|
||||
ln -sfn "$steam_root/linux32" "$HOME/.steam/sdk32"
|
||||
echo "$version" > "$stamp"
|
||||
if [ -n "$fallback" ]; then
|
||||
echo "$stable_channel" > "$channel_state"
|
||||
echo "== STEP $(date -u +%H:%M:%S) the Steam client now follows $stable_channel (beta in ~/.config/droiddeck/steam-channel returns to $beta_channel)"
|
||||
fi
|
||||
if [ -n "$repair" ]; then
|
||||
echo repaired > "$unconfirmed"
|
||||
rm -f "$repair_request"
|
||||
|
||||
@@ -520,39 +520,8 @@ ln -sfn "$STEAM_X64/ubuntu12_32" "$X64_HOME/.steam/bin32"
|
||||
ln -sfn "$STEAM_X64/ubuntu12_64" "$X64_HOME/.steam/bin64"
|
||||
ln -sfn "$STEAM_X64/linux32" "$X64_HOME/.steam/sdk32"
|
||||
ln -sfn "$STEAM_X64/linux64" "$X64_HOME/.steam/sdk64"
|
||||
# The client's bwrap probe (steam-runtime-check-requirements) needs user namespaces, which no Android app has.
|
||||
srt_real=$STEAM_X64/steamrt64/pv-runtime/steam-runtime-steamrt
|
||||
srt_shim=$X64_HOME/steamrt-shim
|
||||
if [ -d "$srt_real" ]; then
|
||||
rm -rf "$srt_shim"
|
||||
mkdir -p "$srt_shim/bin"
|
||||
for e in "$srt_real"/* "$srt_real"/.[!.]*; do
|
||||
[ -e "$e" ] || continue
|
||||
[ "$(basename "$e")" = bin ] || ln -s "$e" "$srt_shim/$(basename "$e")"
|
||||
done
|
||||
for e in "$srt_real"/bin/*; do ln -s "$e" "$srt_shim/bin/$(basename "$e")"; done
|
||||
rm -f "$srt_shim/bin/steam-runtime-check-requirements"
|
||||
printf '#!/bin/sh
|
||||
exit 0
|
||||
' > "$srt_shim/bin/steam-runtime-check-requirements"
|
||||
chmod 755 "$srt_shim/bin/steam-runtime-check-requirements"
|
||||
rm -f "$srt_shim/bin/steam-runtime-launch-client"
|
||||
cat > "$srt_shim/bin/steam-runtime-launch-client" <<'LAUNCH'
|
||||
#!/bin/sh
|
||||
while [ $# -gt 0 ]; do
|
||||
case $1 in
|
||||
--) shift; break ;;
|
||||
--directory=*) cd "${1#--directory=}" || exit 127 ;;
|
||||
--env=*) export "${1#--env=}" ;;
|
||||
--unset-env=*) unset "${1#--unset-env=}" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
[ $# -gt 0 ] || exit 0
|
||||
exec "$@"
|
||||
LAUNCH
|
||||
chmod 755 "$srt_shim/bin/steam-runtime-launch-client"
|
||||
fi
|
||||
# Valve's Steam Runtime container needs user namespaces and a session-bus launcher; steamrt-shim stands in for both.
|
||||
droiddeck-steamrt-shim "$STEAM_X64/steamrt64" "$X64_HOME/steamrt-shim" || true
|
||||
# Valve's platform libraries ship without soname links (pressure-vessel makes them); ldconfig adds them here.
|
||||
srt_libs=$X64_HOME/steamrt-libs
|
||||
rm -rf "$srt_libs"
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# droiddeck-steamrt-shim <client runtime dir> <shim dir>
|
||||
# Builds <shim dir> from the Steam Runtime container the client carries and exits 0, or removes it and exits 1
|
||||
# when the client carries none.
|
||||
set -eu
|
||||
client=$1
|
||||
shim=$2
|
||||
srt_real=$client/pv-runtime/steam-runtime-steamrt
|
||||
if [ ! -d "$srt_real" ]; then
|
||||
probe=$(find "$client" -maxdepth 4 -name steam-runtime-check-requirements -path '*/bin/*' -print -quit 2>/dev/null || true)
|
||||
srt_real=${probe:+${probe%/bin/*}}
|
||||
fi
|
||||
if [ -z "$srt_real" ] || [ ! -d "$srt_real" ]; then
|
||||
rm -rf "$shim"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$shim"
|
||||
mkdir -p "$shim/bin"
|
||||
for e in "$srt_real"/* "$srt_real"/.[!.]*; do
|
||||
[ -e "$e" ] || continue
|
||||
[ "$(basename "$e")" = bin ] || ln -s "$e" "$shim/$(basename "$e")"
|
||||
done
|
||||
for e in "$srt_real"/bin/*; do
|
||||
[ -e "$e" ] || continue
|
||||
ln -s "$e" "$shim/bin/$(basename "$e")"
|
||||
done
|
||||
# The client's bwrap probe (steam-runtime-check-requirements) needs user namespaces, which no Android app has.
|
||||
rm -f "$shim/bin/steam-runtime-check-requirements"
|
||||
printf '#!/bin/sh
|
||||
exit 0
|
||||
' > "$shim/bin/steam-runtime-check-requirements"
|
||||
chmod 755 "$shim/bin/steam-runtime-check-requirements"
|
||||
# Valve's launcher asks a service on the session bus; proot is already the container, so the command runs here.
|
||||
rm -f "$shim/bin/steam-runtime-launch-client"
|
||||
cat > "$shim/bin/steam-runtime-launch-client" <<'LAUNCH'
|
||||
#!/bin/sh
|
||||
while [ $# -gt 0 ]; do
|
||||
case $1 in
|
||||
--) shift; break ;;
|
||||
--directory=*) cd "${1#--directory=}" || exit 127 ;;
|
||||
--env=*) export "${1#--env=}" ;;
|
||||
--unset-env=*) unset "${1#--unset-env=}" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
[ $# -gt 0 ] || exit 0
|
||||
exec "$@"
|
||||
LAUNCH
|
||||
chmod 755 "$shim/bin/steam-runtime-launch-client"
|
||||
@@ -56,7 +56,7 @@ cp "$TEST_CDN/${url##*/}" "$out"
|
||||
self.env = dict(os.environ, HOME=str(self.home), XDG_DATA_HOME=str(self.data),
|
||||
PATH=str(self.bin) + os.pathsep + os.environ['PATH'],
|
||||
TEST_CDN=str(self.cdn), TEST_REQUESTS=str(self.requests))
|
||||
for channel in ('steamdeck_publicbeta',):
|
||||
for channel in ('steamdeck_publicbeta', 'steamdeck_stable'):
|
||||
archive = self.cdn / (channel + '.zip')
|
||||
with zipfile.ZipFile(archive, 'w') as package:
|
||||
package.writestr('steamrtarm64/steam', channel)
|
||||
@@ -68,12 +68,17 @@ cp "$TEST_CDN/${url##*/}" "$out"
|
||||
f'\t\t"file" "{archive.name}"\n\t\t"sha2" "{digest}"\n'
|
||||
'\t}\n}\n')
|
||||
|
||||
def install(self, fail_host=None):
|
||||
def install(self, fail_host=None, *args, **extra):
|
||||
env = self.env if fail_host is None else dict(self.env, TEST_FAIL_HOST=fail_host)
|
||||
return subprocess.run(['bash', str(SCRIPT)], env=env, text=True, capture_output=True)
|
||||
return subprocess.run(['bash', str(SCRIPT), *args], env=dict(env, **extra), text=True, capture_output=True)
|
||||
|
||||
def assert_installed(self, channel='steamdeck_publicbeta', fail_host=None, expected=None):
|
||||
result = self.install(fail_host)
|
||||
def channel(self, **extra):
|
||||
result = self.install(None, '--channel', **extra)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
return result.stdout.strip()
|
||||
|
||||
def assert_installed(self, channel='steamdeck_publicbeta', fail_host=None, expected=None, **extra):
|
||||
result = self.install(fail_host, **extra)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(channel + '\n', (self.steam / 'package/beta').read_text())
|
||||
self.assertEqual('123456\n', (self.steam / 'package/droiddeck-installed').read_text())
|
||||
@@ -87,6 +92,7 @@ cp "$TEST_CDN/${url##*/}" "$out"
|
||||
'https://client-update.steamstatic.com/' + channel + '.zip',
|
||||
]
|
||||
self.assertEqual(expected, self.requests.read_text().splitlines())
|
||||
return result
|
||||
|
||||
def test_downloads_the_deck_beta_client(self):
|
||||
self.assert_installed()
|
||||
@@ -155,8 +161,80 @@ cp "$TEST_CDN/${url##*/}" "$out"
|
||||
self.requests.unlink()
|
||||
with (package / 'droiddeck-unconfirmed').open('a') as marker:
|
||||
marker.write('start\nstart\nstart\n')
|
||||
self.assertEqual(0, self.install(BL_STEAM_CHANNEL='beta').returncode)
|
||||
self.assertFalse(self.requests.exists())
|
||||
|
||||
def test_the_default_channel_is_the_deck_beta(self):
|
||||
self.assertEqual('steamdeck_publicbeta', self.channel())
|
||||
|
||||
def stable_requests(self, beta_manifest=True):
|
||||
stable = [
|
||||
'https://client-update.steamstatic.com/steam_client_steamdeck_stable_linuxarm64',
|
||||
'https://client-update.steamstatic.com/steamdeck_stable.zip',
|
||||
]
|
||||
if beta_manifest:
|
||||
stable.insert(0, 'https://client-update.steamstatic.com/steam_client_steamdeck_publicbeta_linuxarm64')
|
||||
return stable
|
||||
|
||||
def test_a_repaired_beta_client_that_still_shows_nothing_falls_back_to_stable(self):
|
||||
package = self.break_client(2)
|
||||
self.assertEqual(0, self.install().returncode)
|
||||
self.requests.unlink()
|
||||
with (package / 'droiddeck-unconfirmed').open('a') as marker:
|
||||
marker.write('start\nstart\n')
|
||||
result = self.assert_installed('steamdeck_stable', expected=self.stable_requests(False))
|
||||
self.assertIn("falling back to Valve's steamdeck_stable Steam client", result.stdout)
|
||||
self.assertEqual('steamdeck_stable\n', (package / 'droiddeck-channel').read_text())
|
||||
self.assertEqual('repaired\n', (package / 'droiddeck-unconfirmed').read_text())
|
||||
self.assertEqual('steamdeck_stable', self.channel())
|
||||
|
||||
def test_the_stable_fallback_is_not_repeated(self):
|
||||
self.test_a_repaired_beta_client_that_still_shows_nothing_falls_back_to_stable()
|
||||
self.requests.unlink()
|
||||
with (self.steam / 'package/droiddeck-unconfirmed').open('a') as marker:
|
||||
marker.write('start\nstart\nstart\n')
|
||||
self.assertEqual(0, self.install().returncode)
|
||||
self.assertFalse(self.requests.exists())
|
||||
self.assertEqual('steamdeck_stable\n', (self.steam / 'package/beta').read_text())
|
||||
|
||||
def test_a_pinned_beta_channel_never_falls_back(self):
|
||||
package = self.break_client(2)
|
||||
self.assertEqual(0, self.install().returncode)
|
||||
with (package / 'droiddeck-unconfirmed').open('a') as marker:
|
||||
marker.write('start\nstart\n')
|
||||
self.assertEqual('steamdeck_publicbeta', self.channel(BL_STEAM_CHANNEL='beta'))
|
||||
self.assertEqual(0, self.install(BL_STEAM_CHANNEL='beta').returncode)
|
||||
self.assertFalse((package / 'droiddeck-channel').exists())
|
||||
|
||||
def test_the_channel_setting_selects_stable(self):
|
||||
setting = self.home / '.config/droiddeck/steam-channel'
|
||||
setting.parent.mkdir(parents=True)
|
||||
setting.write_text('stable\n')
|
||||
self.assertEqual('steamdeck_stable', self.channel())
|
||||
self.assert_installed('steamdeck_stable', expected=self.stable_requests(False))
|
||||
self.assertFalse((self.steam / 'package/droiddeck-channel').exists())
|
||||
|
||||
def empty_beta_manifest(self):
|
||||
(self.cdn / 'steam_client_steamdeck_publicbeta_linuxarm64').write_text(
|
||||
'"linuxarm64"\n{\n\t"version" "123457"\n'
|
||||
'\t"client_steamrtarm64_linuxarm64"\n\t{\n'
|
||||
'\t\t"file" "client.zip"\n\t\t"sha2" "00"\n'
|
||||
'\t}\n}\n')
|
||||
|
||||
def test_a_beta_manifest_without_an_arm64_client_falls_back_to_stable(self):
|
||||
self.empty_beta_manifest()
|
||||
result = self.assert_installed('steamdeck_stable', expected=self.stable_requests())
|
||||
self.assertIn('no client components in steam_client_steamdeck_publicbeta_linuxarm64', result.stderr)
|
||||
self.assertIn('client_steamrtarm64_linuxarm64', result.stderr)
|
||||
self.assertEqual('steamdeck_stable\n', (self.steam / 'package/droiddeck-channel').read_text())
|
||||
self.assertEqual('steamdeck_stable', self.channel())
|
||||
|
||||
def test_a_pinned_beta_manifest_without_an_arm64_client_fails_and_names_its_blocks(self):
|
||||
self.empty_beta_manifest()
|
||||
result = self.install(BL_STEAM_CHANNEL='beta')
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertIn('client_steamrtarm64_linuxarm64', result.stderr)
|
||||
self.assertFalse((self.steam / 'package/droiddeck-installed').exists())
|
||||
|
||||
def test_requested_repair_runs_once_and_is_retried_until_it_completes(self):
|
||||
package = self.break_client(0)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
BIN = Path(__file__).resolve().parents[1] / 'linuxfs/overlay/usr/local/bin'
|
||||
SHIM = BIN / 'droiddeck-steamrt-shim'
|
||||
SESSION = (BIN / 'droiddeck-session').read_text()
|
||||
X64 = (BIN / 'droiddeck-steam-x64').read_text()
|
||||
|
||||
|
||||
class SteamrtShimTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.client = self.root / 'steamrtarm64'
|
||||
self.client.mkdir()
|
||||
self.shim = self.root / 'shim'
|
||||
|
||||
def runtime(self, path):
|
||||
runtime = self.client / path
|
||||
(runtime / 'bin').mkdir(parents=True)
|
||||
(runtime / 'bin/steam-runtime-check-requirements').write_text('#!/bin/sh\nexit 1\n')
|
||||
(runtime / 'bin/steam-runtime-system-info').write_text('#!/bin/sh\necho info\n')
|
||||
(runtime / 'files').mkdir()
|
||||
(runtime / 'files/marker').write_text('runtime files')
|
||||
return runtime
|
||||
|
||||
def build(self):
|
||||
return subprocess.run(['bash', str(SHIM), str(self.client), str(self.shim)],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
|
||||
def sh(self, *args):
|
||||
return subprocess.run(['sh', *map(str, args)], capture_output=True, text=True, timeout=30)
|
||||
|
||||
def assert_shim(self):
|
||||
result = self.build()
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(0, self.sh(self.shim / 'bin/steam-runtime-check-requirements').returncode)
|
||||
self.assertEqual('info\n', self.sh(self.shim / 'bin/steam-runtime-system-info').stdout)
|
||||
self.assertEqual('runtime files', (self.shim / 'files/marker').read_text())
|
||||
launched = self.sh(self.shim / 'bin/steam-runtime-launch-client', '--env=BL_SHIM_TEST=yes',
|
||||
f'--directory={self.root}', '--', 'sh', '-c', 'echo "$BL_SHIM_TEST $(basename "$PWD")"')
|
||||
self.assertEqual(f'yes {self.root.name}\n', launched.stdout)
|
||||
|
||||
def test_a_client_without_a_container_gets_no_shim(self):
|
||||
self.shim.mkdir()
|
||||
self.assertEqual(1, self.build().returncode)
|
||||
self.assertFalse(self.shim.exists())
|
||||
|
||||
def test_the_pressure_vessel_runtime_is_shimmed(self):
|
||||
self.runtime('pv-runtime/steam-runtime-steamrt')
|
||||
self.assert_shim()
|
||||
|
||||
def test_a_requirements_probe_elsewhere_is_shimmed(self):
|
||||
self.runtime('steam-runtime')
|
||||
self.assert_shim()
|
||||
|
||||
def test_the_arm64_session_starts_a_containerized_client_through_the_shim(self):
|
||||
block = re.search(r'\n( +steam_client=\("\$steam_root/steamrtarm64/steam"\)\n.*?\n fi\n)', SESSION, re.S).group(1)
|
||||
stub = self.root / 'droiddeck-steamrt-shim'
|
||||
for status, shimmed in ((1, False), (0, True)):
|
||||
stub.write_text(f'#!/bin/bash\nexit {status}\n')
|
||||
stub.chmod(0o755)
|
||||
script = 'if :; then\n' + block + 'printf "%s\\n" "${steam_client[@]}"\n'
|
||||
env = dict(os.environ, HOME=str(self.root), steam_root='/steam',
|
||||
PATH=str(self.root) + os.pathsep + os.environ['PATH'])
|
||||
result = subprocess.run(['bash', '-c', script], env=env, capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
lines = result.stdout.splitlines()
|
||||
self.assertEqual('/steam/steamrtarm64/steam', lines[-1])
|
||||
self.assertEqual(shimmed, 'Valve containerized the arm64 Steam client' in result.stdout)
|
||||
self.assertEqual(shimmed, any(line.startswith('STEAM_RUNTIME_STEAMRT=') and
|
||||
line.endswith('/.droiddeck-steamrt-shim') for line in lines))
|
||||
|
||||
def test_both_clients_are_told_they_already_run_inside_the_container(self):
|
||||
guard = SESSION.index('export STEAM_STEAMRT_RECURSION_GUARD=1')
|
||||
self.assertLess(guard, SESSION.index('"${steam_client[@]}" "${_bl_ui[@]}"'))
|
||||
self.assertIn('-u STEAM_STEAMRT_RECURSION_GUARD', SESSION[:guard])
|
||||
self.assertIn('export STEAM_STEAMRT_RECURSION_GUARD=1', X64)
|
||||
self.assertIn('droiddeck-steamrt-shim "$STEAM_X64/steamrt64" "$X64_HOME/steamrt-shim"', X64)
|
||||
|
||||
|
||||
class SessionChannelTest(unittest.TestCase):
|
||||
BLOCK = re.search(r'\n( +steam_channel=steamdeck_publicbeta\n.*?\n +fi\n)', SESSION, re.S).group(1)
|
||||
|
||||
def channel(self, x64, installer):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
stub = Path(temp) / 'droiddeck-steam-install'
|
||||
stub.write_text('#!/bin/bash\n' + installer + '\n')
|
||||
stub.chmod(0o755)
|
||||
env = dict(os.environ, steam_x64=x64, PATH=temp + os.pathsep + os.environ['PATH'])
|
||||
result = subprocess.run(['bash', '-c', self.BLOCK + 'echo "$steam_channel"\n'], env=env,
|
||||
capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
return result.stdout.strip()
|
||||
|
||||
def test_the_arm64_client_follows_the_installed_channel(self):
|
||||
self.assertEqual('steamdeck_stable', self.channel('0', '[ "$1" = --channel ] && echo steamdeck_stable'))
|
||||
|
||||
def test_an_installer_without_an_answer_keeps_the_deck_beta(self):
|
||||
self.assertEqual('steamdeck_publicbeta', self.channel('0', 'exit 1'))
|
||||
|
||||
def test_the_x86_client_keeps_its_own_channel(self):
|
||||
self.assertEqual('steamdeck_publicbeta', self.channel('1', 'echo steamdeck_stable'))
|
||||
|
||||
def test_the_client_branch_on_disk_and_clientbeta_use_the_same_channel(self):
|
||||
self.assertIn('-clientbeta "$steam_channel"', SESSION)
|
||||
self.assertIn('echo "$steam_channel" > "$steam_beta"', SESSION)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user