diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7b58b85..8cb0914 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ -# A pushed vX.Y.Z tag becomes a draft GitHub Release with the macOS build and -# the Windows installers attached; someone publishes it by hand after checking -# it (#68, #348). Started by hand instead, it builds without publishing and +# A pushed vX.Y.Z tag becomes a draft GitHub Release with the macOS build, the +# Windows installers and the Linux packages attached; someone publishes it by +# hand after checking it (#68, #348, #356). Started by hand instead, it builds without publishing and # keeps each platform's files as a run artifact, to test the workflow without # cutting a tag. # @@ -120,9 +120,93 @@ jobs: path: dist/ if-no-files-found: error + linux: + # Each CPU builds on a runner of its own rather than cross-building, so + # each runner can install and start what it built (#356). Both on 24.04, + # whose AppArmor limits on sandboxes are the ones Ubuntu users have. + strategy: + matrix: + include: + - runner: ubuntu-24.04 + arch: x64 + - runner: ubuntu-24.04-arm + arch: arm64 + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + + - name: Tag matches app version + if: github.event_name == 'push' + run: | + version=$(node -p "require('./app/package.json').version") + if [ "$GITHUB_REF_NAME" != "v$version" ]; then + echo "::error::Tag $GITHUB_REF_NAME does not match app/package.json version $version" + exit 1 + fi + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + + - run: npm ci + + # The RPM maker runs `rpmbuild`; the runner has dpkg and fakeroot for + # the .deb already. + - name: Install packaging tools + run: | + sudo apt-get update + sudo apt-get install -y rpm + + - name: Build + run: npm run make -w app + + # The makers already put the CPU in each name, the way each distro + # spells it: pbnotes__amd64.deb, pbnotes--1.x86_64.rpm. + - name: Collect the packages + run: | + shopt -s nullglob + debs=(app/out/make/deb/*/*.deb) + rpms=(app/out/make/rpm/*/*.rpm) + [ ${#debs[@]} -eq 1 ] && [ ${#rpms[@]} -eq 1 ] || { + echo "::error::Expected one .deb and one .rpm under app/out/make, found: ${debs[*]} ${rpms[*]}" + exit 1 + } + mkdir dist + cp "${debs[0]}" "${rpms[0]}" dist/ + ls -l dist + + # Installed the way a user would, then started on a virtual screen. The + # app's code is the same on both CPUs; this is for packaging mistakes. + - name: Smoke-test the .deb + run: | + sudo apt-get install -y xvfb x11-utils ./dist/*.deb + app/scripts/smoke-launch.sh pbnotes + + # In a Fedora container, as a user who isn't root: Electron won't run as + # root without --no-sandbox. The .rpm's sandbox relies on unprivileged + # user namespaces, which Fedora allows; the runner's Ubuntu kernel and + # Docker's defaults don't, so both are lifted for the container. + - name: Smoke-test the .rpm + run: | + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + docker run --rm \ + --security-opt seccomp=unconfined --security-opt apparmor=unconfined \ + -v "$PWD:/w:ro" fedora:latest bash -euc ' + dnf install -y /w/dist/*.rpm xorg-x11-server-Xvfb xwininfo util-linux + useradd -m tester + runuser -u tester -- /w/app/scripts/smoke-launch.sh pbnotes + ' + + - uses: actions/upload-artifact@v4 + with: + name: pbnotes-linux-${{ matrix.arch }} + path: dist/ + if-no-files-found: error + release: if: github.event_name == 'push' - needs: [macos, windows] + needs: [macos, windows, linux] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 diff --git a/README.md b/README.md index 58c0e17..353e5a2 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,33 @@ The app is not yet signed, so the first time Windows shows "Windows protected yo To uninstall, go to Settings → Apps → Installed apps, find Pb Notes and choose **Uninstall**. Your notes stay where they are: they live in the folders you picked, not in the app. +### Linux + +Download the package for your distribution and your PC's processor: + +| Distribution | Intel or AMD | ARM | +| --- | --- | --- | +| Debian, Ubuntu, Linux Mint, Pop!_OS | `pbnotes_…_amd64.deb` | `pbnotes_…_arm64.deb` | +| Fedora, RHEL, openSUSE | `pbnotes-…x86_64.rpm` | `pbnotes-…arm64.rpm` | + +Not sure which processor? `uname -m` prints `x86_64` for Intel or AMD, and `aarch64` for ARM. + +Install it from the folder you downloaded it to. On Debian or Ubuntu: + +```sh +sudo apt install ./pbnotes_*.deb +``` + +On Fedora or RHEL (on openSUSE, `sudo zypper install` instead): + +```sh +sudo dnf install ./pbnotes-*.rpm +``` + +Pb Notes then shows up among your apps. Neither package updates itself yet: to get a new version, download and install it the same way. + +To uninstall, run `sudo apt remove pbnotes` or `sudo dnf remove pbnotes`. Your notes stay where they are: they live in the folders you picked, not in the app. + ## Development The repo is an npm workspace: `app/` and the packages under `packages/`. Install once, at the root: diff --git a/app/forge.config.ts b/app/forge.config.ts index 0a56573..d23076c 100644 --- a/app/forge.config.ts +++ b/app/forge.config.ts @@ -11,6 +11,13 @@ import { execFileSync } from 'node:child_process'; import { readdirSync } from 'node:fs'; import path from 'node:path'; +const linuxPackage = { + icon: 'assets/icon.png', + genericName: 'Notes', + categories: ['Office' as const], + homepage: 'https://github.com/shovon/pbnotes', +}; + const config: ForgeConfig = { packagerConfig: { asar: true, @@ -55,8 +62,10 @@ const config: ForgeConfig = { // consume later (#67). new MakerDMG({ icon: 'assets/icon.icns' }), new MakerZIP({}, ['darwin']), - new MakerRpm({ options: { icon: 'assets/icon.png' } }), - new MakerDeb({ options: { icon: 'assets/icon.png' } }), + // Both write the same .desktop entry. `categories` would otherwise be + // GNOME;GTK;Utility, and `genericName` the app's own name again. + new MakerRpm({ options: linuxPackage }), + new MakerDeb({ options: linuxPackage }), ], // No publishers: `release.yml` makes the draft Release from every // platform's build at once (#348), so Forge publishes nothing itself. diff --git a/app/scripts/smoke-launch.sh b/app/scripts/smoke-launch.sh new file mode 100755 index 0000000..b264428 --- /dev/null +++ b/app/scripts/smoke-launch.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Starts an installed Pb Notes on a virtual screen and checks that its window +# shows and that it is still running a few seconds later (#356). This catches a +# package that installs but can't start: the wrong CPU's Electron, a missing +# library, a sandbox that won't come up. +# +# smoke-launch.sh [args...] +# +# Needs Xvfb and xwininfo. The app gets a throwaway HOME, so it never sees real +# settings or folders. +set -euo pipefail + +[ $# -ge 1 ] || { + echo "usage: $0 [args...]" >&2 + exit 2 +} + +tmp=$(mktemp -d) +app= +xvfb= +cleanup() { + [ -n "$app" ] && kill -TERM -- "-$app" 2>/dev/null || true + [ -n "$xvfb" ] && kill "$xvfb" 2>/dev/null || true + wait 2>/dev/null || true + rm -rf "$tmp" +} +trap cleanup EXIT + +fail() { + echo "::error::$1" + echo "--- app output ---" + cat "$tmp/app.log" || true + exit 1 +} + +# Xvfb picks a free display and writes its number to fd 3. +Xvfb -displayfd 3 -screen 0 1280x800x24 -nolisten tcp 3>"$tmp/display" 2>"$tmp/xvfb.log" & +xvfb=$! +for _ in $(seq 100); do + [ -s "$tmp/display" ] && break + sleep 0.1 +done +[ -s "$tmp/display" ] || { + cat "$tmp/xvfb.log" + echo "::error::Xvfb did not start" + exit 1 +} + +mkdir -p "$tmp/home" +# Unset WAYLAND_DISPLAY so that, run from a Wayland desktop, the app still +# opens on the virtual screen and not the real one. +env -u WAYLAND_DISPLAY \ + DISPLAY=":$(cat "$tmp/display")" \ + HOME="$tmp/home" \ + XDG_CONFIG_HOME="$tmp/home/.config" \ + XDG_DATA_HOME="$tmp/home/.local/share" \ + setsid "$@" >"$tmp/app.log" 2>&1 & +app=$! + +# The window's title is "Pb Notes" and its class "pbnotes". +shown= +for _ in $(seq 60); do + kill -0 "$app" 2>/dev/null || fail "Pb Notes exited before its window showed" + if DISPLAY=":$(cat "$tmp/display")" xwininfo -root -tree | grep -Eq '"Pb Notes"|"pbnotes"'; then + shown=1 + break + fi + sleep 1 +done +[ -n "$shown" ] || fail "No Pb Notes window after 60 seconds" + +sleep 5 +kill -0 "$app" 2>/dev/null || fail "Pb Notes exited within 5 seconds of showing its window" + +echo "Pb Notes started and showed its window."