Installing ChatGPT Desktop on Mint 20 / Ubuntu 20.04 — when the blocker isn't glibc
OpenAI shipped an official ChatGPT Desktop for Linux in August 2026 (preview), supporting Ubuntu 24.04/26.04, Debian 13, Fedora 43/44 and Arch. Mint 20.3 (focal, glibc 2.31) is nowhere on that list, so I expected a rerun of the Claude Desktop surgery.
It wasn’t. The glibc floor was fine all along — the package is blocked by three library renames and one optional TPM module. The fix is smaller, and the audit is worth showing because the conclusion is the opposite of last time.
Start by reading the control file
Before extracting a single byte, look at what the package actually claims:
|
|
The interesting line:
|
|
libc6 (>= 2.30). 2.30. Focal has 2.31.
That single number changes the whole shape of the problem. Last time
dpkg-shlibdeps had computed >= 2.34 and the entire exercise was proving the
declaration was an overstatement. Here the declaration is honest and already
satisfied. Whatever is breaking, it isn’t libc.
Verify it anyway
Trusting a Depends: line is how you end up with a half-configured package. Same
audit as last time — max GLIBC symbol version across every ELF file:
|
|
45 ELF files. The top of the list:
| Component | Max glibc | OK on 2.31? |
|---|---|---|
sky_linux_x64 (the CUA agent binary) |
2.30 | yes |
resources/cua_node/bin/node |
2.28 | yes |
ChatGPT (the Electron binary) |
2.25 | yes |
libvk_swiftshader.so, browser_crashpad_handler |
2.17 | yes |
libEGL.so, libGLESv2.so, libvulkan.so.1 |
2.16 | yes |
Nothing above 2.30. Max GLIBCXX is 3.4.26, and focal’s libstdc++6 is at
13.1.0 (GLIBCXX 3.4.31) here, so C++ is clear too.
A handful of files report no GLIBC symbols at all — resources/codex,
resources/rg, tectonic — those are statically linked Rust binaries. They
don’t care what your libc is.
So what is blocking it?
|
|
|
|
apt reports these one at a time, which is tedious. Check all of them at once:
|
|
Three tss2 packages, plus libgdk-pixbuf-2.0-0, plus libssl3. And here is the
thing worth internalising:
| Declared | Reality on focal |
|---|---|
libgdk-pixbuf-2.0-0 |
renamed upstream; focal ships libgdk-pixbuf2.0-0 |
libtss2-esys-3.0.2-0 |
soname libtss2-esys.so.0 is in libtss2-esys0 |
libtss2-mu0 |
soname libtss2-mu.so.0 — also in libtss2-esys0 |
libtss2-tcti-device0 |
soname libtss2-tcti-device.so.0 — also in libtss2-esys0 |
libssl3 (>= 3.0.0) |
genuinely absent; focal has OpenSSL 1.1 |
Four of the five are pure packaging archaeology. Debian split libtss2-esys0
into per-soname packages after focal, and renamed gdk-pixbuf’s runtime package.
The files have been sitting on the system the whole time:
|
|
The one real missing library
libssl3 is the only genuine absence. Find out who wants it:
|
|
Exactly one hit, out of 45:
|
|
One optional native module — the TPM-backed device key used for remote-control
pairing — is the sole consumer of libcrypto.so.3 and of all three tss2 libs.
Different mechanism from last time, identical moral: dpkg-shlibdeps takes the
union over every ELF in the tree, so one auxiliary binary sets the floor for a
1.4 GB package.
Note it needs libcrypto.so.3, not libssl.so.3. The declared libssl3
dependency is the package that happens to ship both.
Don’t be tempted by a stray libssl3
apt-cache policy libssl3 surprised me:
|
|
A Deepin-built OpenSSL 3, from a deepin-wine repo I’d added years ago and
forgotten. It would probably have satisfied the dependency. Installing a
foreign-distro OpenSSL into /usr/lib/x86_64-linux-gnu to make one optional
telemetry-adjacent module load is a terrible trade — and [trusted=yes] on that
repo means no signature check either. Drop the dependency instead.
Test before installing anything
Run the extracted tree directly. No root, no dpkg, apt untouched:
|
|
It launches, renders, reaches chatgpt.com and mounts the renderer in ~3.6 s.
No GLIBC_ errors, no missing-symbol aborts. The TPM module simply never gets
required at startup.
Why env -u LD_LIBRARY_PATH matters
That flag isn’t decoration. My .zshrc exports a long LD_LIBRARY_PATH
covering /usr/local/gcc14/lib64, ~/miniconda3/lib and more. With it set:
|
|
Conda’s OpenSSL 3 “satisfies” the missing library, and gcc14’s libstdc++ shadows
the system one. That is a fake pass: an app launched from the Cinnamon menu
inherits none of it, because .zshrc only runs for interactive shells. Audit
in the same environment the app will actually run in, or you will conclude
something works and then watch it fail from the menu.
Patch the control file only
Last time I used dpkg-deb -R / dpkg-deb -b, which rebuilds the data archive.
Here the payload is 399 MB compressed and 1.4 GB installed — recompressing it to
change five lines of text is pointless. A .deb is just an ar archive of three
members, so replace the control member in place and leave data.tar.xz alone:
|
|
ar r replaces a member while preserving its position, so the required
debian-binary → control.tar.xz → data.tar.xz ordering survives. The whole
repack takes about a second instead of several minutes.
The three tss2 entries collapse onto the same package, so dedupe the result — apt tolerates a repeated dependency, but it looks like a mistake:
|
|
Always dry-run before committing:
|
|
No pulled-in extras, no held packages. That’s the signal you want.
Check the maintainer scripts
|
|
Two jobs: install OpenAI’s signing key + a deb822 .sources file, and handle
AppArmor. The AppArmor branch is the one to read:
|
|
Focal ships AppArmor 2.13 with no abi/4.0, so it takes the elif and
disables the profile by symlink rather than trying to load a 4.0-syntax profile
that would fail. Well-behaved. Nothing here bites on focal.
Install and hold
|
|
The hold is not optional, for the same reason as last time: postinst
registers
|
|
so the next apt upgrade happily replaces your patched package with the stock
one and re-breaks the dependency tree. With the hold you re-apply the patch per
release — which takes about thirty seconds now that it’s a control-only rewrite.
The .desktop wants to be your browser
Read the shipped entry before you let it register:
|
|
It claims x-scheme-handler/http and https — which makes ChatGPT a candidate
default browser in Cinnamon’s preferred-application list, and a target for any
xdg-open http://... call. Maybe you want that. I didn’t.
Since XDG_DATA_HOME (~/.local/share) is searched before XDG_DATA_DIRS, a
user-level copy shadows the packaged one, survives reinstalls, and needs no root
— the same trick as last time, now doing double duty: proxy wiring and
trimming the MIME claims.
~/.local/bin/chatgpt-proxy:
|
|
The unset is the new part, and it’s the important one. It guarantees a menu
launch and a terminal launch resolve libraries identically — otherwise the app
binds conda’s OpenSSL from your shell and the system’s from the menu, and you get
bugs that “only happen sometimes”.
~/.local/share/applications/chatgpt.desktop:
|
|
|
|
Get StartupWMClass from the running app rather than guessing it — I first wrote
ChatGPT and the taskbar showed an unmatched generic icon:
|
|
The second member of the pair is what StartupWMClass has to match: Chatgpt.
A terminal chatgpt still reaches /usr/bin/chatgpt and bypasses all of this,
so shadow it too — ~/.local/bin comes first on my PATH:
|
|
Note /usr/bin/chatgpt is itself a symlink to ../lib/chatgpt/codex-launcher,
a two-line shell script that resolves its own path and execs ChatGPT. Pointing
the wrapper at /usr/bin/chatgpt is fine and survives updates.
What you lose
- Remote control / device pairing — needs
remote-control-device-key.node, which needslibcrypto.so.3
That’s it. Codex, CUA, chat, file access and MCP all work. Considerably better than the Claude Desktop outcome, where computer-use, the Cowork VM and Chrome native messaging all fell over.
Troubleshooting: it opens, but it isn’t connected
This is the failure I actually hit, and the wrapper was innocent. One command settles it — the app talks to the proxy or it doesn’t:
|
|
A healthy session shows a dozen or more established connections to the proxy port. Zero — or no matching process at all — means something upstream of the proxy went wrong. Three candidates, in the order they cost me time:
1. $http_proxy is not a proxy setting
|
|
Chromium on Cinnamon reads its proxy configuration from GSettings, not from the
environment. Every HTTP_PROXY export in my .zshrc is invisible to it. Without
an explicit --proxy-server the app goes direct, and direct is blocked — so the
env exports in the wrapper are there for the bundled Node/codex child
processes, and the --proxy-server flag is what actually moves the Electron
traffic. Both are load-bearing, for different consumers.
If you would rather fix it once for every Electron app on the machine:
|
|
2. chatgpt was not the app I thought it was
|
|
The function predated the official package and pointed at
/snap/bin/chatgpt-desktop-client — an unrelated third-party snap (v1.1.1) I had
installed back when there was no Linux build. Typing chatgpt in a terminal
launched that, with its own empty session, and of course it wasn’t connected.
The official app was never involved.
Check type/command -v before drawing any conclusion about “the app”. A name
that has been on your PATH for a year is not evidence about a package you
installed this morning.
3. A stale instance swallows your flags
Electron enforces single-instance through ~/.config/Codex/SingletonLock. If a
proxy-less instance is already running, launching the wrapper just focuses the
existing window and discards its arguments entirely. Check before testing:
|
|
A trap I didn’t fall into, but nearly
My .zshrc also had a helper that “added” the proxy on top of the wrapper:
|
|
The wrapper already passes --proxy-server=http://..., so the process gets the
switch twice, and Chromium keeps the last occurrence of a command-line switch,
not the first. Proved by appending a deliberately dead
--proxy-server=socks5://127.0.0.1:9, which produced 0 sockets to the proxy
— the working flag had been thrown away.
Here it happened to be harmless: 20170 is a mixed port that answers HTTP proxy and SOCKS5, so the override landed on the same working endpoint.
|
|
(403 from Cloudflare to a bare curl still means the request got there.) Change
that port to an HTTP-only proxy and the same line breaks the app with no
diagnostic beyond “not connected”. Don’t layer proxy flags — set them in one
place, which here is the wrapper.
A red herring in the log
This line shows up in a perfectly healthy session:
|
|
That is the remote-control pairing that needs remote-control-device-key.node
— the one thing we knowingly gave up. hostId=local reaching state=connected,
and [chatgpt-account-lookup] ... result=succeeded, are the lines that tell you
the network is fine.
Takeaway
Last time the lesson was don’t trust an inflated libc6 bound — audit it.
This time the bound was accurate and the lesson is the mirror image: an
uninstallable .deb on an old release is usually not an ABI problem at all.
Before assuming your glibc is too old, ask which of these you actually have:
- A renamed package. Debian/Ubuntu reshuffle runtime package names between
releases (
libgdk-pixbuf-2.0-0, the tss2 split, thet64transition). The soname is the contract; the package name is not. Check withdpkg -Sandldconfig -pbefore believing anything is missing. - One optional component setting the floor.
dpkg-shlibdepsunions over every ELF file. Find the outlier withobjdump -p | grep NEEDED. - An actual ABI wall. Rare, and
objdump -Twill prove it in one command.
And run your audit with a clean environment. A stray LD_LIBRARY_PATH will
cheerfully tell you everything resolves, right up until you launch from the
application menu.
相关内容
- Installing Claude Desktop on Ubuntu 20.04 / Mint 20 (glibc 2.31)
- glibc2.18 安装
- (转)Linux 网络大流量传输优化方法
- 在 c 代码文件插入 shell 命令
- tmux: error while loading shared libraries: libevent_core 2.1.so.6
支付宝
微信

william