// Created At 2026-11-01
// Troubleshooting · Python · Bash

Steam Does Nothing When You Click It: Tracing Two Layered Faults

Environment: Ubuntu 24.04, Intel Arc A770, Wayland session, steam-launcher installed via apt. (Neither the GPU nor Wayland plays any part later on — Steam renders its window through XWayland and both were irrelevant to this fault. Same caveat for i386, but version-scoped: on the client build used here, a system without i386 necessarily crashes at the point where steamui.so is loaded; whether other builds behave identically is something only the actual log can tell you. They are listed only because they are facts about this machine.)

Structurally, this turned out to be two direct faults plus one upstream root cause: the client never finished downloading, and 32-bit libraries were missing. The second one happened because a shadowed python3 on PATH made steamdeps fail silently (section 7).

Currency and sample size: written in November 2026, client build 1792618285. Package names, manifest structure and download-host behaviour all change between releases; every speed figure and log excerpt here is a single sample from this one machine on this one network. Take the method with you, don’t treat the conclusions as settled.

Reader and prerequisites: this assumes you know roughly what dpkg, ldd and strace do, and does not explain their flags. Without that background you might still fix the machine by copying commands, but you won’t understand why it worked.

If you just want it working and don’t care how: try a game accelerator or proxy first — switch it to global/TUN mode, restart Steam, and let it download the client update itself. Worth a try, no guarantee: accelerator rules often don’t cover Steam’s update domain (there’s a way to test this in section 3). If that fails, read on.

1. Symptoms

Click the icon, nothing happens. Wait a while, click again, still nothing. No window, no error dialog, nothing at all.

The easiest mistake here is guessing from impressions. My first instinct was “leftover lock files” — because ~/.steam/ was sitting there with a pile of suspicious-looking things:

starting
steam.pid        (contents: 5518)
~/.local/share/Steam/.crash

Deleted them, restarted, no change.

Those files only prove “the last exit wasn’t clean” — not “nothing is running now”. One pgrep -af steam showed Steam was alive and well, busy doing something else entirely.

2. The Truth Is in the Logs

~/.local/share/Steam/logs/ holds a dozen or so files. The three that matter:

  • bootstrap_log.txt — the launcher itself (the only one with content while the client isn’t installed yet)
  • console-linux.txt — the launch script plus the client
  • updateui_child.txt — that little updater window

These logs are flooded with progress lines; reading them raw tells you nothing. Filter them out first:

grep -v -E "Downloading update|Add pending" ~/.local/share/Steam/logs/bootstrap_log.txt

What’s left is the skeleton. The first thing that stands out is this pair of lines:

[19:20:26] [  2%] Downloading update (13,816 of 502,699 KB)...
...
[19:20:56] [  0%] Downloading update (3,866 of 502,699 KB)...

The counter fell from 13,816 back to 3,866 and the percentage reset to zero. This isn’t a slow download, it’s a failed download retrying from scratch. It had been spinning in that loop for a long time.

Now grep for errors:

Error: Download of package (tenfoot_images_all) failed after 0 bytes (0 : 200).
Error: Download of package (public_all) failed after 0 bytes (0 : 0).

(0 : 200) means the HTTP status came back 200 but zero bytes actually arrived — connection established, server said “fine”, then the stream was cut. (0 : 0) means not even an HTTP response arrived. Seeing both shapes together is a strong signal of a link problem, not a Steam or disk problem.

That settles the first fault: the client itself (about 500 MB) had never finished downloading. So “won’t open” was really just me watching an updater spin in place.

Worth being precise about the phrase “installed”: the steam-launcher package is only a launcher. apt installing it successfully does not mean the client is present — the real client has to be fetched over the network by the launcher itself. Strictly speaking, “installed” only covered step one.

3. Finding a Route That’s Alive

Every launch, the launcher prints the download hosts it intends to use at the top of the log. That is the authoritative source:

Using the following download hosts for Public, Realm steamglobal
1. https://client-update.steamstatic.com, /, Realm 'steamglobal', weight was 1, source = 'baked in'

source = 'baked in' means this address is compiled into the binary as a default, not read from config.

Now test it yourself. Don’t copy IPs out of someone else’s article — that’s their network topology, not yours. The method: grab any package from the manifest, run it with a time limit, and look at average throughput.

# Pick any filename from the manifest (the one below will go stale — use your own)
URLPATH="public_all.zip.vz.54c72b908da906e7af3bee17206b30830d6f627a_25274088"
for h in client-update.steamstatic.com \
         client-update.akamai.steamstatic.com \
         steamcdn-a.akamaihd.net ; do
    printf "%-42s " "$h"
    curl -s -o /dev/null --max-time 12 \
         -w "%{speed_download} B/s\n" "https://$h/$URLPATH"
done

My result was extreme:

Host Result
client-update.steamstatic.com (default) stalled after 2.4 MB of 25 MB, then timed out entirely
client-update.akamai.steamstatic.com 2.2 MB/s, completes
steamcdn-a.akamaihd.net 196 B/s
cdn.steamstatic.com 216 B/s

Four orders of magnitude between domains on the same CDN. The default one was dead.

Aside: If You Do Have an Accelerator or Proxy

Everything above — the speed testing and the pre-placement — is the path for people without a proxy. If you have one, just use it, but be aware of two traps. First, setting only the http_proxy environment variable does not reliably affect the updater — it has its own network stack, so you need to intercept at the network layer (TUN, transparent proxy). Second, accelerator rules often cover only the store and community and miss client-update, in which case you’re back to section 4. Whether it’s working is decided by whether the log counter keeps climbing (test described in section 2). Whatever you use, comply with your local laws and the service’s terms.

4. Pre-Place the Packages (No root, No Proxy)

The idea came from one log line. Steam verifies the local installation before anything else:

Verifying installation...
Verifying file sizes only
Verification complete

Verifying file sizes only — it compares sizes, nothing more. Which means: drop the files into ~/.local/share/Steam/package/ under the correct names and correct sizes, and it won’t download them again. No root needed, no proxy needed, just one route that actually works.

The flow is short: read the Add pending download: / Downloading manifest: lines from bootstrap_log.txt to learn which files are wanted and where the manifests live, parse out filename, size and sha256, then download from a working route into package/ and verify each one. Sounds straightforward, but I fell into three traps.

Trap 1: A .vz File’s Real Size Is in Its Filename

The manifest looks like this:

"tenfoot_images_all"
{
	"file"    "tenfoot_images_all.zip.86419c7a56c12dd107b5e0d46f50c8a9b121f3cc"
	"size"    "6582204"
	"sha2"    "990676bc29d01736477c6b571b5048..."
	"zipvz"   "tenfoot_images_all.zip.vz.193cb8c4eb4446698ea2c0a9e8c4e6b6a623dac7_5572671"
	"sha2vz"  "e1fee3beffa9d08a415a35ddc7d3141af6c44269..."
}

size / sha2 describe the uncompressed zip; zipvz / sha2vz are the compressed file you actually download. The link between them is the number at the end of the filename: _5572671 is the byte size of the .vz.

My first script compared .vz files against size, declared a batch of perfectly good downloads “verification failed”, and deleted them — a wasted round. The correct rule: if zipvz exists, use the number in its suffix as the size and sha2vz as the hash; only the few entries without zipvz use size + sha2.

Trap 2: There Is More Than One Manifest

Feed in only the ubuntu12 one and, after a restart, brand-new names appear in the log — bins_steamrt64, runtime_steamrt_steamrt64 and so on. The steam_client_steamrt64 manifest has to be fetched separately.

Trap 3 (Saves Half the Traffic): The Two Manifests Ship Identical Content Under Different Names

The comparison is obvious once you look — same sha2vz, same size, two different names:

bins_codecs_ubuntu12.zip.vz.0ceb4808651cbce441edfa95ae49474c40a6ed01_12851097
bins_codecs_steamrt64.zip.vz.0ceb4808651cbce441edfa95ae49474c40a6ed01_12851097

So the second manifest doesn’t need a fresh download. Build a “hash → existing path” index and copy existing files to their new names. Of my 493 MB second manifest, only one 89 MB file was genuinely re-downloaded; the other 27 were reused.

The Last Step: Verify the Hashes Yourself

“Size is the only check” is both what makes this method work and what makes it risky: if a file’s contents are corrupt, Steam will not notice for you. So run sha256sum yourself against the manifest’s sha2vz once the download finishes.

What happens if a bad file does slip in? The symptom is unexplained crashes or UI misbehaviour after launch, with Steam reporting nothing — at which point you delete the corresponding file under ~/.local/share/Steam/package/, let it re-download, and you’re back to a clean state.

After filling everything in, a restart produced this at last:

Installing update...
Extracting package...
Update complete, launching Steam...

The first layer was through.

Appendix: A Runnable Skeleton

The flow above consolidates into two files. Don’t reach for jq — the manifest is Valve’s KeyValues format (VDF), not JSON, and jq fails with parse error: Expected separator between values.

Parse the manifest into three tab-separated columns (filename, size, sha256):

vdf_packages.py (Python)
#!/usr/bin/env python3
import re, sys

def load(path):
    text = open(path, encoding="utf-8", errors="replace").read()
    toks = re.findall(r'"[^"]*"|[{}]|[^\s{}"]+', text)
    root, stack, key = {}, [{}], None
    for t in toks:
        if t == "{":
            d = {}; stack[-1][key] = d; stack.append(d); key = None
        elif t == "}":
            if len(stack) > 1: stack.pop()
            key = None
        elif t.startswith('"'):
            if key is None: key = t[1:-1]
            else: stack[-1][key] = t[1:-1]; key = None
    return stack[0]

def packages(node):
    for v in node.values():
        if not isinstance(v, dict): continue
        if "zipvz" in v:                     # compressed: real size is the filename suffix
            fn = v["zipvz"]
            yield fn, fn.rsplit("_", 1)[1], v["sha2vz"]
        elif "file" in v and "size" in v:     # the few uncompressed entries
            yield v["file"], v["size"], v["sha2"]
        else:
            yield from packages(v)

for name, size, sha in packages(load(sys.argv[1])):
    print(f"{name}\t{size}\t{sha}")

Triage / reuse / download / verify, with the route you measured in section 3 as the argument:

steam_prefetch.sh (bash)
#!/bin/bash
# steam_prefetch.sh — pre-place Steam client update packages into the package dir
# usage: ./steam_prefetch.sh https://<a download host that works>
#   e.g. ./steam_prefetch.sh https://client-update.akamai.steamstatic.com
set -u

HOST="${1:?usage: $0 https://<download host>}"
PKG="$HOME/.local/share/Steam/package"
HERE="$(cd "$(dirname "$0")" && pwd)"
mkdir -p "$PKG"

# 1) Fetch the manifests. List as many realms as the launcher wants — copy them
#    from the "Downloading manifest:" lines in bootstrap_log.txt
MANIFESTS="steam_client_ubuntu12 steam_client_steamrt64"

LIST=$(mktemp)
QUEUE=$(mktemp)
trap 'rm -f "$LIST" "$QUEUE" "$LIST".*' EXIT
for m in $MANIFESTS; do
    curl -sS --fail -m 60 -o "$LIST.$m" "$HOST/$m" || { echo "cannot fetch manifest $m"; exit 1; }
    python3 "$HERE/vdf_packages.py" "$LIST.$m"
done | sort -u > "$LIST"
echo "manifest lists $(wc -l < "$LIST") packages"

# 2) Triage: already correct -> skip; same content under another name -> copy locally; else -> queue
#    Build the hash->path index once, so the loop doesn't re-hash the whole dir every pass
declare -A H2F
for f in "$PKG"/*; do
    [ -f "$f" ] && H2F["$(sha256sum "$f" | cut -d' ' -f1)"]="$f"
done

while IFS=$'\t' read -r fn size sha; do
    out="$PKG/$fn"
    if [ -f "$out" ] && [ "$(stat -c%s "$out")" = "$size" ] &&
       [ "$(sha256sum "$out" | cut -d' ' -f1)" = "$sha" ]; then
        continue
    fi
    src="${H2F[$sha]:-}"
    if [ -n "$src" ] && cp --reflink=auto -f "$src" "$out" 2>/dev/null; then
        echo "reused $(basename "$out")"
        continue
    fi
    printf '%s\t%s\t%s\n' "$fn" "$size" "$sha" >> "$QUEUE"
done < "$LIST"

# 3) Download + verify (use xargs -P 6 for concurrency when there are many files)
while IFS=$'\t' read -r fn size sha; do
    tmp="$PKG/$fn.part"
    if curl -sS --fail --retry 5 --retry-all-errors --max-time 1200 -C - -o "$tmp" "$HOST/$fn" &&
       [ "$(stat -c%s "$tmp")" = "$size" ] &&
       [ "$(sha256sum "$tmp" | cut -d' ' -f1)" = "$sha" ]; then
        mv -f "$tmp" "$PKG/$fn"
        echo "OK   $fn"
    else
        echo "FAIL $fn"
        rm -f "$tmp"
    fi
done < "$QUEUE"

I ran this skeleton for real on this machine, and its triage result matched trap 3 above. The single exception was webkit_ubuntu12 — Steam deleted it after installing, and it differs from the steamrt64 copy by one byte, so the hashes didn’t match and it couldn’t be reused. It was the only file actually re-downloaded.

It has known limits: good enough as a troubleshooting tool, not good enough as a long-term one. The parser covers only the common subset of Steam manifests (it doesn’t handle escaped quotes inside strings, and a duplicate key takes the last value); downloads are serial, with no automatic multi-route fallback and a fixed --max-time 1200; the reuse index is built once at the start, so files downloaded during the current run never enter it. For long-term use you’d have to fill all of that in.

5. Layer Two: ELFCLASS64

The update finished, the launcher exited cleanly and pulled up the client. Then the process was gone again.

Error: You are missing the following 32-bit libraries, and Steam may not run:
libGL.so.1
libdrm.so.2
dlmopen /home/dark/.local/share/Steam/ubuntu12_32/steamui.so failed:
        libGL.so.1: 错误的 ELF 类:ELFCLASS64
Fatal error: Failed to load steamui.so

ELFCLASS64 is the most informative token in that whole error. It doesn’t mean “library not found” — it means “found it, but the 64-bit one”. A 32-bit steamui.so asks for libGL.so.1 and the system hands it the 64-bit build; the ELF classes don’t match. (The message in the middle is localized on this machine — the last token is the part that matters.)

Dig one level down:

$ file ~/.local/share/Steam/ubuntu12_32/steamui.so
ELF 32-bit LSB shared object, Intel 80386 ...

$ dpkg --print-foreign-architectures
(empty)

Empty output — this machine had never enabled the i386 architecture, so there wasn’t a single 32-bit library on the system. And Steam’s UI layer is 32-bit.

This layer and the first one are two entirely independent faults. Fixing the download doesn’t mean the launch works, and conversely: if your network is fine and it still won’t start, this is the only layer you need to deal with.

6. Installing the 32-bit Libraries

sudo dpkg --add-architecture i386
sudo apt update
sudo apt install steam-libs-i386

steam-libs-i386 is Ubuntu’s metapackage for Steam; it pulls in over a hundred i386 packages, including libGL, libdrm, libX11 and libasound (160 packages in one go on this machine).

Package names change between distribution releases — 24.04 renamed a large batch of them with a -t64 suffix because of the 64-bit time_t transition. So confirm a package exists before copying the command:

apt-cache policy libgtk2.0-0t64:i386

Which libraries are still missing is ultimately decided by what ldd actually prints.

But installing that isn’t the end of it. At this point steamui.so was still missing 6 libraries — the metapackage’s list simply doesn’t include them:

libXtst.so.6  libXrandr.so.2  libXi.so.6
libgtk-x11-2.0.so.0  libpipewire-0.3.so.0  libxcb-res.so.0
sudo apt install libxtst6:i386 libxrandr2:i386 libxi6:i386 \
                 libgtk2.0-0t64:i386 libpipewire-0.3-0t64:i386 \
                 libxcb-res0:i386

A full sweep of the 32-bit components also turns up libICE.so.6, libSM.so.6 and libopenal.so.1; installing those too is the safer bet.

One small trap I walked into: don’t use /usr/lib32 to decide whether something is installed. i386 libraries live in /usr/lib/i386-linux-gnu/. I checked the wrong directory, saw “still missing” for libraries that were already installed, and nearly went round the loop again.

The right verification isn’t reading the package list, it’s asking the ELF file directly:

cd ~/.local/share/Steam/ubuntu12_32
ldd steamui.so | grep "not found"

Empty output is the only real proof.

If apt reports unmet dependencies while installing these i386 packages, or dpkg reports a configuration failure, you can first run sudo apt install -f to repair the dependency state and then retry. But it cannot replace the list above: -f only fixes packages that are installed with their dependencies unmet, and these 6 libraries are not a dependency of anything installed (apt-cache rdepends --installed libxtst6:i386 is empty), so the system was never in a broken state — a simulated apt install -f -s reports 0 upgraded, 0 newly installed, 0 to remove. Also note that -f may remove packages in order to reach a consistent state, so read the prompt before pressing enter.

7. One More Step Upstream: Why Those Libraries Were Never Installed

The launch script actually tried to install them from the start; it failed and decided to “carry on regardless”:

bin_steam.sh[4456]: Unable to install Steam dependencies by running
    /usr/bin/steamdeps, trying to continue anyway...

That trying to continue anyway is the line that postponed every later problem. Had it errored out and stopped there, the user would never have reached any of the above.

So why did steamdeps fail? Its own account:

Couldn't import apt, please install python3-apt or update steamdeps for your distribution.

But python3-apt was demonstrably installed. Digging further:

$ head -1 /lib/steam/bin_steamdeps.py
#!/usr/bin/env python3

$ which -a python3
/home/dark/.local/bin/python3
/usr/bin/python3

$ /usr/bin/python3        -c "import apt"   →  works
$ ~/.local/bin/python3    -c "import apt"   →  ModuleNotFoundError

~/.local/bin/python3 sorts ahead of the system interpreter on PATH — very common on machines that have installed uv, pyenv, poetry or similar. steamdeps uses env python3 and therefore picked up this shadow interpreter. Meanwhile the apt_pkg module shipped by python3-apt lives in /usr/lib/python3/dist-packages/, visible only to /usr/bin/python3.

You don’t have to take that on inference — run an A/B yourself. Same machine, same script, same python3-apt, only PATH differs:

$ python3 /lib/steam/bin_steamdeps.py --help
Couldn't import apt, please install python3-apt or update steamdeps for your distribution.

$ PATH=/usr/bin:/bin python3 /lib/steam/bin_steamdeps.py --help
usage: bin_steamdeps.py [-h] [--dry-run] [--interactive] [--install INSTALL] ...

Then let strace show you which interpreter it actually execves — no guessing left:

$ strace -f -e trace=execve /lib/steam/bin_steamdeps.py --help 2>&1 | grep execve
execve("/lib/steam/bin_steamdeps.py", ...) = 0
execve("/home/dark/.nvm/.../bin/python3", ...) = -1 ENOENT
execve("/home/dark/.cargo/bin/python3", ...)  = -1 ENOENT
execve("/home/dark/.local/bin/python3", ...)  = 0     ← the shadow interpreter

It also shows that this machine’s PATH is stuffed with nvm, npm, pnpm and cargo bin directories; env python3 walked 9 paths before it found the shadow. That kind of PATH pollution is common on developer machines, so this configuration is not exotic.

So the full causal chain:

shadowed python3 on PATH
    → steamdeps cannot import apt, fails silently
        → 32-bit dependencies never installed, i386 never enabled
            → steamui.so fails to load, client crashes

Breaking any link in that chain works, but the two ways of doing it are not the same kind of thing. Installing the libraries by hand is a workaround: the libraries are present, steamdeps is still broken, and the next time it runs (say a game needs a new 32-bit dependency) it will fail silently again. Moving ~/.local/bin off the front of PATH is the actual fix. I chose the workaround because it was fastest, and the price is that the trap is still sitting there.

One aside on reading the log: that Error: You are missing the following 32-bit libraries line is Steam’s own self-check output, not the cause of the failure. The real red herring was the steamdeps line — it disguised “missing dependency” as “missing python module”.

8. Confirming It Actually Opened

A running process doesn’t mean a window was painted. Two unambiguous checks:

# Does the window exist (Steam runs through XWayland, so xwininfo sees it)
xwininfo -root -tree | grep Steam
#   0x2400039 "Sign in to Steam": ("steamwebhelper" "steam") 700x440+963+516

# Are the UI processes all up (steamwebhelper spawns several zygotes)
pgrep -af steamwebhelper | wc -l

A window titled Sign in to Steam plus a sane process count means it’s up.

9. Three Loose Ends

Stopgap versus long-term. A permanent proxy is the least work; pre-placing packages in package/ sits in between — one operation, no proxy needed afterwards; pinning an IP via /etc/hosts is only for emergencies.

Why hosts is emergency-only. Writing a fast node’s IP into /etc/hosts genuinely works — I verified it with --resolve: TLS validated, file downloaded completely. But it doesn’t last, because Akamai’s edge IPs rotate: two lookups of client-update.akamai.steamstatic.com twenty minutes apart returned 23.221.24.201 and 23.220.255.48. A pinned address goes stale sooner or later.

An operational trap while debugging. pkill -f steam matches your own command line — if the command contains the word “steam”, it kills itself too. I hit this twice in a row, each time with my shell on the receiving end of SIGTERM/SIGKILL. The workaround is a character class in the regex:

pgrep -f "ubuntu12_32/stea[m]"    # matches the target process, not your own command line

10. Retrospective: Seven Judgments

In hindsight none of it is complicated, but the order matters:

  1. First confirm whether it’s even running — don’t rush to clear lock files
  2. Filter the progress lines and read only the log skeleton — a scrolling progress bar is noise
  3. Tell “slow” apart from “failing and retrying” — a counter going backwards means the latter
  4. The numbers and type names in error messages deserve a literal read — (0 : 200) and ELFCLASS64 both pointed straight at the root cause
  5. Verify with tools, not intuition — ldd ... | grep "not found" beats any package list
  6. Faults usually come in layers — fixing the first one doesn’t mean you’re done
  7. Chase down “why didn’t it do this itself” — usually worth more than “doing it by hand”, because my real root cause was hiding in an error swallowed by trying to continue anyway

A failure that degrades silently (continue anyway) is far harder to find than one that crashes loudly.

If this article helped you, you can buy me a coffee ☕️
Ali PayWechat Pay