fix(linux): declare the libraries the packages need, and prove it on a clean machine - #325
Merged
Merged
Conversation
…sked for
The same shape as the glibc ceiling this release already fixed, and invisible to
the guard that fixed it. before-pack.cjs checks how NEW a required symbol is; it
has nothing to say about whether the library carrying it is ever installed. So
1.9.1 shipped three sonames that nothing declared and nothing bundled.
Measured on the published 1.9.1 .deb, installed into a bare ubuntu:22.04:
libgbm.so.1 the Electron binary itself. The app exits 127 on
"error while loading shared libraries" before any window.
libasound.so.2 same binary, same result.
libgomp.so.1 GCC's OpenMP runtime, needed by all 32 ELFs of the STT stack
(whisper-stt-server, the libggml*, libwhisper, libparakeet).
The app starts and only transcription dies in ld.so.
None is in electron-builder's default deb list, which never followed Chromium to
GBM, and `depends` REPLACES that default rather than extending it, so there was
no layer left to catch them.
All three hid behind the same accident: desktop metapackages pull every one, and
libgomp1 only via libfftw3-single3, libimagequant0 and libsoxr0 — three
peripheral media libraries no desktop needs. Every machine anyone tested on had
them, including this one, which is why a package that cannot start on a clean
distro read as fine for two releases. A minimal install or a hand-assembled WM
has none of those guarantees, and the AppImage has no way to declare any of it.
Names resolved per distro rather than guessed, each in a container of that
distro. It matters: libgomp.so.1 is `libgomp1` on Debian but `libgomp` on Fedora
AND on Arch, where it was split out of `gcc-libs` — the obvious guess is wrong.
`libasound2` is right for both Ubuntu 22.04 and 24.04, where the real package is
`libasound2t64` and Provides the short name, exactly as `libgtk-3-0` already
relies on.
Fedora turns out not to have been broken: its base image plus gtk3's closure
already supply all three, and the rpm passes without this change. Declaring them
is what makes that true rather than lucky.
The .pacman artifact of 1.9.1 cannot be installed at all. Not degraded, not
missing a feature — pacman refuses the whole transaction:
warning: cannot resolve "http-parser", a dependency of "openscreen"
error: failed to prepare transaction (could not satisfy dependencies)
:: unable to satisfy dependency 'http-parser' required by openscreen
`http-parser` is part of electron-builder's default pacman `depends`, written
when Node still used it. Node moved to llhttp years ago and Arch dropped the
package; nothing in the repos Provides it today. Nothing in our payload wants it
either — it appears in none of the 49 DT_NEEDED sonames the package carries.
This is the one entry in this file that deliberately departs from the
electron-builder default, against the rule the deb block states, so it gets said
out loud in a comment: removing a default is normally how a package breaks
silently, and this is the case where keeping one is.
`libappindicator-gtk3`, immediately below, is stale in exactly the same way and
was left alone on purpose — `libappindicator` declares it in both Provides and
Replaces, so it still resolves. Checked, not assumed.
Verified on archlinux:latest by installing the published 1.9.1 package with
--assume-installed http-parser, which is precisely what removing the line does:
the install succeeds, all 39 shipped ELFs resolve, and the main binary reaches
main() instead of dying in ld.so.
Found by the verification job that follows this commit, on its first run.
…lled Three Linux releases in a row shipped a dependency the target machine could not resolve, and each fix arrived with a guard aimed at the failure already understood. The colocation check would never have caught the glibc floor; the symbol-version check would never have caught a library that is simply not installed anywhere. Both are worth keeping and neither generalises, which is the actual problem — a static check only ever knows about the mistakes already made. Windows reached this conclusion one release earlier (verify-appx-native.ps1); this is the same answer for Linux. So ask the loader. verify-linux-package.sh installs a built package into a BARE container of the target distro, runs ldd over every ELF that ships, and reports any soname the package neither declares nor bundles. Then it starts openscreen, whisper-stt-server and openscreen-pipewire-helper, because reaching main() is the part ldd cannot show: a binary the loader rejects exits 127 with "error while loading shared libraries", while one that starts prints its own usage or its own structured error. The container is the point rather than an implementation detail. Every failure of this shape has one cause — the machines we build on have more installed than the machines we ship to — so a check running on the runner is not a check. For the same reason the probe installs no tooling inside the container: binutils would arrive with a transitive closure that could mask what is being measured, and ldd is glibc, already there. Two things it gets right that a first attempt did not. Libraries are judged by whether they ship, not by RUNPATH: the bundled ffmpeg objects carry no RUNPATH of their own and reach libavutil only through the binary that loads them, so ldd run against one of THEM reports three sonames missing that are present and fine. And it asserts it found at least ten ELFs, because a probe that silently stops looking reports clean forever after. rpm and pacman are covered too, and they are the ones with no other safety net: nobody installs them often enough to report a gap quickly. That paid for itself immediately — the pacman run is what surfaced http-parser, an entry that made the Arch package uninstallable and that no amount of reading the list would have revealed. Verified both ways before wiring it up. Against the published 1.9.1 .deb it fails with exactly libgbm.so.1, libasound.so.2 and libgomp.so.1 and no false positives; against the same package repacked with those three declared it passes, 39 ELFs resolving and all three executables reaching main(). The negative case is the one that matters here. The AppImage is deliberately not covered. It has no dependency mechanism, so there is no declaration to verify against and every system soname is missing by construction. It stays exposed — that is what d3d_linux::diagnose naming the Mesa package is for. The job runs before the upload step, and publish-release needs build-linux, so a package that cannot resolve blocks both its own artifact and the release.
…at made it The Discord announcement was a step of prerelease.yml and promote.yml, so it could only ever announce what those two workflows did. A release cut by pushing a tag straight at build.yml — which is how 1.9.2 ships — runs neither, and would have gone out silent. That is the same class of miss the standalone dispatch was extracted for after v1.9.0, one level up: the recovery path was fixed and the trigger was not. Move it onto `release: published`, the one event every path shares, and read `github.event.release.prerelease` for the stable/rc split that used to be hand-passed. This works only because build.yml creates releases with OPENSCREEN_RELEASE_TOKEN — a release created with the default GITHUB_TOKEN triggers no workflows, and this change would silently do nothing. It also fixes a bug in the RC path that was invisible while it lived there: prerelease.yml announced immediately after DISPATCHING build.yml rather than after it completed, so #rc-testing was told to go test a build whose artifacts would not exist for another twenty minutes. `release: published` means, by definition, that the assets are attached. Both call sites are removed rather than left as a fallback, because two triggers on one event post twice. Nothing is lost with them: promote.yml passed RC_TAG to get a "Promoted from <rc>" line the script only ever renders under `isRc && rcTag`, and it passed KIND: stable, so that line has never appeared in a stable announcement. `release_notes_extra` no longer reaches Discord automatically — prepending something stays possible through the dispatch inputs, which are kept. STRICT is now "1" on every path. It was "0" inside the ceremonies for a real reason — the release was already out and a failed announcement had to not report it as broken — and standing alone inverts that: nothing depends on this job, so a red run costs a notification while a green run that posted nothing recreates the exact failure this exists to prevent.
Contributor
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (9)
📝 WalkthroughWalkthroughThe release workflows now announce published releases through one workflow. Linux packages declare additional runtime dependencies and undergo clean-container verification for ELF resolution and executable startup. ChangesRelease announcement flow
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant ReleaseEvent
participant announce-release.yml
participant Discord
ReleaseEvent->>announce-release.yml: publish release
announce-release.yml->>announce-release.yml: resolve tag and release kind
announce-release.yml->>Discord: post release announcement
sequenceDiagram
participant build.yml
participant verify-linux-package.sh
participant CleanContainer
participant LinuxPackage
build.yml->>verify-linux-package.sh: verify package artifact
verify-linux-package.sh->>CleanContainer: mount artifact and select distro image
CleanContainer->>LinuxPackage: install package
CleanContainer->>CleanContainer: scan ELF dependencies
CleanContainer->>CleanContainer: start native executables
CleanContainer-->>build.yml: pass or fail verification
Possibly related PRs
Suggested reviewers: ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Carried in this PR rather than as a separate commit on main, because 1.9.2 does not go through prerelease.yml: there is no RC to promote, so nothing else would write the version, and build.yml's publish guard rejects a tag whose version does not match package.json. Both manifests, via set-release-version.mjs — a sed over package.json alone is what shipped three releases with a lockfile disagreeing with the package it locks.
nix-check.yml only runs on PRs touching package-lock.json or nix/**, so it fires here on a two-line version bump and reports a mismatch this branch did not cause. The recorded hash was pinned to a dependency set that main's lockfile stopped resolving to some time ago — exactly the drift that workflow was added to surface, now surfaced by the first PR to touch the lockfile since. The bump changes two lines, both the `version` field, and no dependency at all, so nothing here alters what npm resolves. Setting it to what `prefetch-npm-deps package-lock.json` reports makes `nix build` work again. That bump also repairs a related inconsistency: main's package-lock.json still said 1.8.0 while package.json said 1.9.0, which is the exact failure set-release-version.mjs exists to prevent and which predates it.
This was referenced Aug 10, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
1.9.1 fixed the symbol-version floor and shipped three sonames that nothing
declared and nothing bundled. Measured on the published .deb, installed into a
bare
ubuntu:22.04:libgbm.so.1libasound.so.2libgomp.so.1ld.sobefore-pack.cjscould not have caught any of it: it checks how new a requiredsymbol is, not whether the library carrying it is ever installed. And nothing
else was watching — the
dependslists are hand-written, and electron-builderpasses fpm none of
--rpm-autoreq*.All three hid behind the same accident. Desktop metapackages pull every one, and
libgomp1only vialibfftw3-single3,libimagequant0andlibsoxr0— threeperipheral media libraries. Every machine anyone tested on had them.
A second, unrelated defect surfaced while verifying the Arch package: it is
uninstallable.
http-parseris part of electron-builder's stale defaultpacman list, Arch dropped the package, nothing Provides it, and pacman refuses
the whole transaction. Nothing in our payload wants it either.
The part that generalises
scripts/verify-linux-package.shinstalls a built package into a bare containerof the target distro, runs
lddover every shipped ELF, and reports any sonamethe package neither declares nor bundles — then starts the three executables,
because reaching
main()is whatlddcannot show. Same answer Windows reachedone release earlier with
verify-appx-native.ps1, and the reasoning is the same:a static check only ever knows about the mistakes already made.
It runs on all three formats before the upload step, so a package that cannot
resolve blocks its own artifact and the release.
rpmandpacmanhad no othersafety net at all — the pacman run is what surfaced
http-parseron its firstexecution.
The AppImage is deliberately not covered: it has no dependency mechanism, so
there is no declaration to verify against.
Discord announcement
Moved onto
release: published, the one event every release path shares. As astep of prerelease.yml/promote.yml it could only announce what those workflows
did, so a release cut by pushing a tag straight at build.yml — which is how 1.9.2
ships — would have gone out silent. It also fixes an RC bug that was invisible
there: prerelease.yml announced right after dispatching build.yml, so
#rc-testing was pointed at artifacts that would not exist for another twenty
minutes.
Verification
.deb: fails with exactly the three sonames, no false positives.PASS, 39 ELFs resolving, all three executables reachingmain().http-parserneutralised it installs and all 39 ELFs resolve.mesa-vulkan-driversremoved:apt installpulls it back as declared, and the compositor preview renders.No application code changes — nothing under
electron/,src/orcrates/. Thepayload is byte-identical to 1.9.1.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation