Skip to content

Fix the Linux packages failing to launch, and verify each installer in CI - #1094

Merged
krassowski merged 13 commits into
jupyterlab:masterfrom
notluquis:ci/snap-launch-smoke-test
Aug 15, 2026
Merged

krassowski merged 13 commits into
jupyterlab:masterfrom
notluquis:ci/snap-launch-smoke-test

Conversation

@notluquis

@notluquis notluquis commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Closes #870. Follow-up to a review comment on #1091.

Three artifacts leave the linux leg and none was ever run. Each now gets launched in CI. The snap passed; the other two failed from the same cause: naming a value in electron-builder's config replaces its default instead of adding to it.

Symptom on first run Cause
deb zygote_host_impl_linux.cc:128] No usable sandbox! on ubuntu-24.04, clean install (#870, open since 2024) deb.fpm's --after-install is appended after the built-in one and fpm keeps the last, so after-install.tpl never ran. The shipped postinst is 104 bytes and has no AppArmor block, though the profile itself is in md5sums
rpm error while loading shared libraries: libnspr4.so rpm.depends: ["libXScrnSaver"], added in 2017, replaced all eight entries of getDefaultDepends('rpm'). REQUIRENAME in the released header has no nss, which is where libnspr4 comes from

The fix vendors app-builder-lib 26.15.7's after-install.tpl plus our two jlab lines, wired through the documented afterInstall option instead of the raw fpm argument, and drops the rpm depends block. A unit test reads the upstream template out of node_modules and fails if the shipped copy drifts from it, so an electron-builder bump that touches the postinst breaks on the bump.

One loose end I did not take on here: nothing owns /usr/bin/jlab. The deb postinst creates it, the snap configure hook creates it, and the snap remove hook deletes it unconditionally, so with both installed the last writer wins. Giving it an owner needs the after-remove template vendored too.

The matrix also gets fail-fast: false and one retry on the installer build. Those are about this job rather than about the packages, so happy to split them out if you would rather review them separately.

One thing fail-fast: false changes that is worth deciding rather than inheriting: the snap publish step lives inside the matrix and gates only on release-exists, while upload-release-assets has needs: publish. So on a release run where one leg fails and linux succeeds, the snap reaches the store and the GitHub assets job is skipped. That race existed before, since cancellation is not instant, but this makes it systematic. Three ways out, and the choice is a release-pipeline call rather than a packaging one: leave it, move the snap publish into a job that needs the whole matrix, or drop fail-fast: false and accept losing three artifacts to one flake.

Worked through this with Claude Code; the postinst, postrm and rpm requires are read out of the released v4.6.2-1 packages, and the two failures plus the green run after them are this branch's own CI.

The check added in jupyterlab#1091 infers that the snap can start: it reads the paths
command.sh execs and requires each to be in the image. That catches the shape of
the 4.6.2-1 failure and not much else, as pointed out in review.

Install the built snap and run it instead. --dangerous is what Canonical
documents for a locally built snap and --classic matches our confinement, and
--version makes the app quit before any window is created, so the whole chain
runs, command.sh into desktop-init.sh into the binary into main.js, with no UI
involved. It is the same shape jupyter_releaser applies to Python dists in
check-release, where a built artifact is installed into a venv and imported
rather than only inspected.

Neither snapd nor xvfb appears in the runner image manifest, so the step installs
what is missing instead of assuming. Whether snapd works on a GitHub-hosted
runner is exactly what the first run of this PR settles; if it does not, the
content check it replaces is still in the history.
snap install hangs on hosted runners often enough to have its own report
(actions/runner-images#11170, closed without a fix), and the timeout here only
covered running the app, not installing it, so a hang would have held the job
until its own limit rather than failing in five minutes.
@notluquis
notluquis requested a review from Copilot July 28, 2026 22:28

This comment was marked as low quality.

@notluquis
notluquis requested a review from krassowski July 28, 2026 22:52
The snap check answers whether one of three Linux artifacts starts. The deb
and the rpm reach more users and neither has ever been run in CI.

Installing the deb through apt also exercises the Depends it declares. Running
the rpm inside an AlmaLinux 9 container is the only way this workflow can say
anything about the RHEL family, since the runner is Ubuntu. jupyterlab#308 has sat open
since 2021 asking about a distro that was never in Electron's verified set, and
a launch check on a maintained rebuild makes that answerable on every release
instead of by argument.

Both steps run after the artifacts are uploaded, so a failure reports the
problem without withholding the build.

Worked through this with Claude Code. I checked the launch path by hand: the
packaged app answers --version with 4.6.2-1 and exits without opening a window,
and the deb places its binary at /opt/JupyterLab/jupyterlab-desktop with only
jlab symlinked into /usr/bin, which is why the steps name absolute paths rather
than trust lookup order once the snap is installed.
@notluquis notluquis changed the title Verify the snap by launching it rather than by reading its contents Verify the Linux installers by launching them rather than by reading their contents Jul 29, 2026
Hosted runners share an outbound IP pool, so anonymous Docker Hub pulls are a
known source of rate limit failures. quay serves the same image with no such
limit.

Recording the image choice while it is fresh: Rocky's Docker Official Image was
last rebuilt in May 2024, AlmaLinux's is current, so the maintained RHEL-family
rebuild to test against is AlmaLinux either way.

Worked through this with Claude Code; both registries were checked for the tag's
last update rather than picked by reputation.
@notluquis
notluquis marked this pull request as draft July 29, 2026 02:32
The deb and rpm pass `--after-install=build/linux_after_install.sh` through
`fpm`, and fpm keeps the last `--after-install` it is given. app-builder-lib
adds its own from the built-in template first, so ours won the argument and
the template never ran.

The shipped v4.6.2-1 deb carries a 104 byte postinst as a result: the jlab
symlink and a chmod. Everything the template does was dropped, including
copying `resources/apparmor-profile` into `/etc/apparmor.d`. The package
already ships that profile, it was simply never installed, which is why the
app aborts on Ubuntu 24.04 where unprivileged user namespaces are restricted.

The script is now the template plus the two jlab lines, wired through the
`afterInstall` option so `${sanitizedProductName}` is substituted the same way
in both halves. `ln -sf` also replaces `ln -s`: the symlink is not in the
package file list, so nothing ever removes it and every upgrade hit
"File exists".

A unit test asserts the vendored copy still starts with the template that
electron-builder ships, so a version bump that changes the postinst shows up
as a failing check on the bump instead of silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKjggvkgBouMRW6DgVBh4q
@notluquis notluquis changed the title Verify the Linux installers by launching them rather than by reading their contents Launch the Linux installers in CI, and fix the postinst that first run exposed Aug 2, 2026
notluquis and others added 2 commits August 2, 2026 16:30
`rpm.depends: ["libXScrnSaver"]` has been in package.json since 2017. It does
not add to electron-builder's defaults, it replaces them, so the shipped rpm
requires exactly one library:

    $ # REQUIRENAME from the v4.6.2-1 rpm header
    /bin/sh
    libXScrnSaver
    rpmlib(...)

The defaults it displaced are gtk3, libnotify, nss, libXScrnSaver, libXtst,
xdg-utils, at-spi2-core and libuuid. Installing the rpm on a clean AlmaLinux 9
therefore pulls in nothing that Electron needs, and the binary stops at

    error while loading shared libraries: libnspr4.so

which comes from nss. libXScrnSaver is in the default list already, so the
override can go rather than be extended.

The test file grows a case for the same shape of mistake and is renamed, since
it now covers the postinst and the dependency list rather than one script.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKjggvkgBouMRW6DgVBh4q
The previous commit staged only the file rename, so the two edits it describes
are here: the `rpm.depends` override goes away, and the test file gains the
case that pins it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKjggvkgBouMRW6DgVBh4q
…stalls

Debian policy 6.5 asks maintainer scripts to be idempotent, and the postinst
had never been run twice against its own output: that is how `ln -s` without
`-f` survived. Unpacking the same deb a second time exercises it.

The step also asserts the AppArmor profile reaches /etc/apparmor.d and is gone
after purge, which is the fix for jupyterlab#870 stated as a check rather than as a
claim in a commit message. That /usr/bin/jlab outlives purge is printed as a
note: the postinst creates it outside the package file list, so nothing owns
it, and closing that means vendoring the after-remove template too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKjggvkgBouMRW6DgVBh4q
@notluquis notluquis changed the title Launch the Linux installers in CI, and fix the postinst that first run exposed Launch the Linux installers in CI, and fix the two bugs that exposed Aug 2, 2026
@notluquis
notluquis marked this pull request as ready for review August 2, 2026 20:47
@notluquis

Copy link
Copy Markdown
Collaborator Author

Green on the full matrix now, run 30766360293. Three commits, three verdicts, all from this branch's own CI:

Commit Linux leg What it showed
e720238 fail deb: No usable sandbox!, exit 133. rpm never reached
ab1a55a pass deb and rpm both launch and print 4.6.2-1
331305f pass reinstall and purge added: profile present after install, absent after purge

The purge step also printed the one thing left open, which is why it is a note rather than an assertion:

note: /usr/bin/jlab is left behind by purge

Ready for review. Worked through with Claude Code; every line above is copied from those three runs.

The matrix builds four independent installers and `fail-fast` defaults to
true, so a flake on any leg cancels the rest. It happened on jupyterlab#1077, where
dmgbuild could not detach its disk image ("hdiutil: couldn't eject disk2 -
Resource busy") and took the Windows build down with it mid-step, leaving no
way to tell whether the other platforms were fine without a full rerun.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKjggvkgBouMRW6DgVBh4q
@notluquis notluquis changed the title Launch the Linux installers in CI, and fix the two bugs that exposed Fix the Linux packages failing to launch, and verify each installer in CI Aug 2, 2026
Three PRs hit "hdiutil: couldn't eject disk2 - Resource busy" in a single day.
It is upstream and long standing (electron-builder#4606, #7137, #9155,
dmgbuild#26 for the DiskArbitration timeouts behind it), dmgbuild already
retries the detach internally, and dmg-builder invokes it with no way to raise
that count from our config, so there is nothing to tune.

One retry costs a few minutes when a build genuinely fails and saves a full
matrix rerun when it does not. Same shape as the codesign retry a few steps
above, which exists for the same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKjggvkgBouMRW6DgVBh4q
The loop form failed the linux leg even though the build succeeded: the step
log ends with yarn's own "Done in 355.00s" and then exit code 1, with no
retry message in between, so the shell returned non-zero after the successful
branch rather than the build failing. The same construct behaves correctly in
a plain login shell here, so the cause is something about the runner's shell
rather than the logic, and chasing it is not worth a CI round trip.

`cmd || { retry }` needs no explicit exit at all: the step ends on the status
of whichever run was last.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DKjggvkgBouMRW6DgVBh4q

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (1)

electron-builder-scripts/linux_after_install.sh:60

  • The comment points to a non-existent test file (test/unit/linux-after-install.test.ts). The unit test added in this PR is test/unit/linux-packaging.test.ts, so the reference is misleading when someone updates the vendored template and needs to find the guard test.
# Everything above is app-builder-lib's after-install.tpl verbatim; only what
# follows is ours. test/unit/linux-after-install.test.ts fails if they diverge.

Nothing asserted /usr/bin/jlab, which is the only line this postinst adds on
top of the vendored template, so a wrong target would have shipped green; the
snap installed earlier in the job claims the same path, hence pinning it.
Xvfb needs a moment to bind its socket before Electron can reach ready.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@notluquis

notluquis commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Ready for review.

Since the last look, four gaps in the checks themselves: /usr/bin/jlab is pinned to its target rather than only tested for existence, the rpm step waits for Xvfb's socket instead of racing it, snap wait system seed.loaded runs best-effort first, and the drift assertion prints what diverged.

Copilot reviewed the current head, no comments.

One call left in the body: fail-fast: false lets a release run publish the snap while the assets job is skipped.

@krassowski krassowski left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@krassowski
krassowski enabled auto-merge (squash) August 15, 2026 15:25
@krassowski
krassowski merged commit 9f08f18 into jupyterlab:master Aug 15, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crash after upgrading to Ubuntu 24.04

3 participants