Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 83 additions & 17 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ jobs:
release_exists: ${{ steps.release-exists.outputs.result }}
version: ${{ steps.package-info.outputs.version }}
strategy:
# Each leg builds an independent installer, so a flake on one platform
# must not cancel the other three and throw away their artifacts.
fail-fast: false
matrix:
cfg:
- { platform: linux-64, platform_name: Linux x64, os: ubuntu-latest, build_platform: linux-64, conda_platform: linux-64 }
Expand Down Expand Up @@ -159,7 +162,15 @@ jobs:
env:
CSC_IDENTITY_AUTO_DISCOVERY: false # disable code signing if not release asset
run: |
yarn dist:${{ matrix.cfg.platform }}
# dmgbuild intermittently cannot detach its disk image on hosted mac
# runners ("hdiutil: couldn't eject disk2 - Resource busy", upstream
# electron-builder#4606 and dmgbuild#26). Three separate PRs hit it in
# one day, so retry once rather than spend a full matrix rerun on it.
yarn dist:${{ matrix.cfg.platform }} || {
echo "installer build failed, retrying once"
sleep 15
yarn dist:${{ matrix.cfg.platform }}
}
if: steps.release-exists.outputs.result == 'false'

- name: Create Application Installer for Release
Expand All @@ -172,7 +183,15 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.CSC_LINK }}
run: |
yarn dist:${{ matrix.cfg.platform }}
# dmgbuild intermittently cannot detach its disk image on hosted mac
# runners ("hdiutil: couldn't eject disk2 - Resource busy", upstream
# electron-builder#4606 and dmgbuild#26). Three separate PRs hit it in
# one day, so retry once rather than spend a full matrix rerun on it.
yarn dist:${{ matrix.cfg.platform }} || {
echo "installer build failed, retrying once"
sleep 15
yarn dist:${{ matrix.cfg.platform }}
}
if: steps.release-exists.outputs.result == 'true'

- name: Upload Debian x64 Installer
Expand Down Expand Up @@ -203,25 +222,72 @@ jobs:
- name: Verify the snap can actually launch
if: matrix.cfg.platform == 'linux-64'
run: |
command -v unsquashfs >/dev/null || { sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends squashfs-tools; }
# 4.6.2-1 shipped the template as a tarball and died on a missing script
unsquashfs -q -d "$RUNNER_TEMP/snap-check" dist/JupyterLab.snap command.sh
unsquashfs -l -d squashfs-root dist/JupyterLab.snap | sort > "$RUNNER_TEMP/snap-have.txt"
grep -oE '\$SNAP/[A-Za-z0-9._/-]+' "$RUNNER_TEMP/snap-check/command.sh" | sed 's|\$SNAP|squashfs-root|' | sort -u > "$RUNNER_TEMP/snap-need.txt"
if [ ! -s "$RUNNER_TEMP/snap-need.txt" ]; then
echo "::error::no \$SNAP path was read out of command.sh, so this check would pass without verifying anything"
exit 1
fi
missing="$(comm -23 "$RUNNER_TEMP/snap-need.txt" "$RUNNER_TEMP/snap-have.txt")"
# neither snapd nor xvfb is listed in the runner image manifest, so do
# not assume either is there
missing=""
command -v snap >/dev/null || missing="$missing snapd"
command -v xvfb-run >/dev/null || missing="$missing xvfb"
if [ -n "$missing" ]; then
echo "::error::command.sh execs paths that are not in the snap: $missing"
exit 1
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends $missing
fi
if grep -qE '^squashfs-root/snap-template-.*\.tar$' "$RUNNER_TEMP/snap-have.txt"; then
echo "::error::the snap template was packed as a tarball instead of being extracted"
exit 1
# --dangerous skips assertion checks for a locally built snap, --classic
# matches the confinement we ship with
# snap install is known to hang on hosted runners
# (actions/runner-images#11170), so bound it rather than let it eat the
# job's budget
sudo timeout 300 snap install --dangerous --classic dist/JupyterLab.snap
# --version quits before any window is created, so this exercises
# command.sh and every launcher script it execs, without needing a UI
xvfb-run -a timeout 120 jupyterlab-desktop --version

- name: Verify the deb can actually launch
if: matrix.cfg.platform == 'linux-64'
run: |
# apt resolves the Depends the deb declares, so an under-declared
# dependency fails here rather than on a user's machine
sudo apt-get install -y ./dist/JupyterLab.deb
# the deb keeps its binary under /opt and only symlinks jlab into
# /usr/bin, so name the path and do not race the snap installed above
xvfb-run -a timeout 120 /opt/JupyterLab/jupyterlab-desktop --version

- name: Verify the deb installs, reinstalls and removes cleanly
if: matrix.cfg.platform == 'linux-64'
run: |
# Debian policy 6.5 requires maintainer scripts to be idempotent, and
# unpacking over the copy installed above is the cheapest way to run
# the postinst a second time against the state the first one left
sudo dpkg -i ./dist/JupyterLab.deb
# the profile ships in the package but only reaches apparmor.d if the
# postinst installs it, which is what #870 turned on
test -f /etc/apparmor.d/jupyterlab-desktop
sudo apt-get purge -y jupyterlab-desktop
test ! -e /etc/apparmor.d/jupyterlab-desktop
# a note rather than an assertion: the postrm drops the alternatives
# entry but not the jlab symlink, which the postinst creates outside
# the package file list
if [ -L /usr/bin/jlab ]; then
echo 'note: /usr/bin/jlab is left behind by purge'
fi

- name: Verify the rpm can actually launch
if: matrix.cfg.platform == 'linux-64'
run: |
# the runner is Ubuntu, so the rpm needs a RHEL-family userland of its
# own. AlmaLinux 9 is a maintained rebuild sitting above every floor
# Electron states for Linux, which are Debian 10, Fedora 32 and
# Ubuntu 18.04. Rocky's Docker Official Image stopped being rebuilt in
# 2024, and quay serves the same AlmaLinux image without Docker Hub's
# anonymous pull limit, which hosted runners share an IP pool against
docker run --rm -v "$PWD/dist:/dist:ro" quay.io/almalinuxorg/almalinux:9 bash -c '
set -e
dnf -y install /dist/JupyterLab.rpm xorg-x11-server-Xvfb
Xvfb :99 -screen 0 1024x768x24 &
# Chromium cannot set up its sandbox as root inside a container, and
# this only needs to answer whether the binary runs on this libc
DISPLAY=:99 timeout 120 /opt/JupyterLab/jupyterlab-desktop --version --no-sandbox
'

- name: Upload macOS x64 Installer
if: matrix.cfg.platform == 'osx-64'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
Expand Down
65 changes: 63 additions & 2 deletions electron-builder-scripts/linux_after_install.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,63 @@
ln -s "/opt/JupyterLab/resources/app/jlab" /usr/bin/jlab
chmod 755 "/opt/JupyterLab/resources/app/jlab"
#!/bin/bash

if type update-alternatives >/dev/null 2>&1; then
# Remove previous link if it doesn't use update-alternatives
if [ -L '/usr/bin/${executable}' -a -e '/usr/bin/${executable}' -a "`readlink '/usr/bin/${executable}'`" != '/etc/alternatives/${executable}' ]; then
rm -f '/usr/bin/${executable}'
fi
update-alternatives --install '/usr/bin/${executable}' '${executable}' '/opt/${sanitizedProductName}/${executable}' 100 || ln -sf '/opt/${sanitizedProductName}/${executable}' '/usr/bin/${executable}'
else
ln -sf '/opt/${sanitizedProductName}/${executable}' '/usr/bin/${executable}'
fi

# Check if user namespaces are supported by the kernel and working with a quick test:
if ! { [[ -L /proc/self/ns/user ]] && unshare --user true; }; then
# Use SUID chrome-sandbox only on systems without user namespaces:
chmod 4755 '/opt/${sanitizedProductName}/chrome-sandbox' || true
else
chmod 0755 '/opt/${sanitizedProductName}/chrome-sandbox' || true
fi

if hash update-mime-database 2>/dev/null; then
update-mime-database /usr/share/mime || true
fi

if hash update-desktop-database 2>/dev/null; then
update-desktop-database /usr/share/applications || true
fi

# Install apparmor profile. (Ubuntu 24+)
# First check if the version of AppArmor running on the device supports our profile.
# This is in order to keep backwards compatibility with Ubuntu 22.04 which does not support abi/4.0.
# In that case, we just skip installing the profile since the app runs fine without it on 22.04.
#
# Those apparmor_parser flags are akin to performing a dry run of loading a profile.
# https://wiki.debian.org/AppArmor/HowToUse#Dumping_profiles
#
# Unfortunately, at the moment AppArmor doesn't have a good story for backwards compatibility.
# https://askubuntu.com/questions/1517272/writing-a-backwards-compatible-apparmor-profile
if apparmor_status --enabled > /dev/null 2>&1; then
APPARMOR_PROFILE_SOURCE='/opt/${sanitizedProductName}/resources/apparmor-profile'
APPARMOR_PROFILE_TARGET='/etc/apparmor.d/${executable}'
if apparmor_parser --skip-kernel-load --debug "$APPARMOR_PROFILE_SOURCE" > /dev/null 2>&1; then
cp -f "$APPARMOR_PROFILE_SOURCE" "$APPARMOR_PROFILE_TARGET"

# Updating the current AppArmor profile is not possible and probably not meaningful in a chroot'ed environment.
# Use cases are for example environments where images for clients are maintained.
# There, AppArmor might correctly be installed, but live updating makes no sense.
if ! { [ -x '/usr/bin/ischroot' ] && /usr/bin/ischroot; } && hash apparmor_parser 2>/dev/null; then
# Extra flags taken from dh_apparmor:
# > By using '-W -T' we ensure that any abstraction updates are also pulled in.
# https://wiki.debian.org/AppArmor/Contribute/FirstTimeProfileImport
apparmor_parser --replace --write-cache --skip-read-cache "$APPARMOR_PROFILE_TARGET"
fi
else
echo "Skipping the installation of the AppArmor profile as this version of AppArmor does not seem to support the bundled profile"
fi
fi

# 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.

ln -sf '/opt/${sanitizedProductName}/resources/app/jlab' /usr/bin/jlab
chmod 755 '/opt/${sanitizedProductName}/resources/app/jlab'
11 changes: 2 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,10 @@
]
},
"deb": {
"fpm": [
"--after-install=build/linux_after_install.sh"
]
"afterInstall": "build/linux_after_install.sh"
},
"rpm": {
"depends": [
"libXScrnSaver"
],
"fpm": [
"--after-install=build/linux_after_install.sh"
]
"afterInstall": "build/linux_after_install.sh"
},
"snap": {
"confinement": "classic",
Expand Down
53 changes: 53 additions & 0 deletions test/unit/linux-packaging.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';

const repoRoot = path.resolve(__dirname, '../..');

const read = (relative: string) =>
fs.readFileSync(path.join(repoRoot, relative), 'utf8');

// The deb and rpm postinst runs as root and installs an AppArmor profile, so it
// has to stay in step with the electron-builder release we ship against.
describe('the Linux after-install script', () => {
it('carries the app-builder-lib template it is derived from, unedited', () => {
const upstream = read(
'node_modules/app-builder-lib/templates/linux/after-install.tpl'
);

const ours = read('electron-builder-scripts/linux_after_install.sh');

expect(ours.startsWith(upstream)).toBe(true);
});

it('links jlab into the PATH on top of what the template does', () => {
const ours = read('electron-builder-scripts/linux_after_install.sh');

expect(ours).toContain(
"ln -sf '/opt/${sanitizedProductName}/resources/app/jlab' /usr/bin/jlab"
);
});

it('is wired into both package targets through electron-builder', () => {
const build = JSON.parse(read('package.json')).build;

for (const target of [build.deb, build.rpm]) {
expect(target.afterInstall).toBe('build/linux_after_install.sh');
expect(target.fpm ?? []).not.toContain(
'--after-install=build/linux_after_install.sh'
);
}
});
});

// Naming even one dependency replaces electron-builder's whole default list
// instead of adding to it, and those defaults (getDefaultDepends in
// app-builder-lib's FpmTarget) are what Electron needs in order to load at all.
// An rpm that named only libXScrnSaver could not find libnspr4.
describe('the Linux runtime dependencies', () => {
it.each(['deb', 'rpm'])('are left to electron-builder for the %s', target => {
const build = JSON.parse(read('package.json')).build;

expect(build[target].depends).toBeUndefined();
});
});
Loading