Fix the Linux packages failing to launch, and verify each installer in CI - #1094
Conversation
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.
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.
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.
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
`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
|
Green on the full matrix now, run 30766360293. Three commits, three verdicts, all from this branch's own CI:
The purge step also printed the one thing left open, which is why it is a note rather than an assertion: 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
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
There was a problem hiding this comment.
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 istest/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.
|
Ready for review. Since the last look, four gaps in the checks themselves: Copilot reviewed the current head, no comments. One call left in the body: |
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.
zygote_host_impl_linux.cc:128] No usable sandbox!on ubuntu-24.04, clean install (#870, open since 2024)deb.fpm's--after-installis appended after the built-in one and fpm keeps the last, soafter-install.tplnever ran. The shipped postinst is 104 bytes and has no AppArmor block, though the profile itself is inmd5sumserror while loading shared libraries: libnspr4.sorpm.depends: ["libXScrnSaver"], added in 2017, replaced all eight entries ofgetDefaultDepends('rpm'). REQUIRENAME in the released header has nonss, which is where libnspr4 comes fromThe fix vendors app-builder-lib 26.15.7's
after-install.tplplus our two jlab lines, wired through the documentedafterInstalloption instead of the raw fpm argument, and drops the rpmdependsblock. A unit test reads the upstream template out ofnode_modulesand 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 snapconfigurehook creates it, and the snapremovehook 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: falseand 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: falsechanges that is worth deciding rather than inheriting: the snap publish step lives inside the matrix and gates only onrelease-exists, whileupload-release-assetshasneeds: 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 dropfail-fast: falseand 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.